Compare commits

...
13 Commits
Author SHA1 Message Date
gisle 624f7046af server: Added resilience to trust proxy resolution to handle IP changes and bad start order. 2019-01-19 11:18:44 +01:00
gisle 8b3d642104 logs: Added default limit that's cleared by adding a filter or clicking show more at the bottom.
the build was successful
2019-01-12 15:35:21 +01:00
gisle 46369f84bd data: Fixed incosnsitensies and undefines in history.
the build was successful
2019-01-12 14:34:59 +01:00
gisle 68f3effa8d story-content: Added new checks for story tag management so any logged-in user can view the form if the story is open.
the build was successful
2019-01-12 12:18:55 +01:00
gisle 4657ae2aec logs: Added prioritization of filter names starting with the search query, for example bringing 'Li' higher up when the search query is 'li'.
the build was successful
2019-01-05 11:46:46 +01:00
gisle f134d57b79 data: Fixed centiseconds being used instead of seconds in change history entry timestamps.
the build was successful
2019-01-05 11:31:25 +01:00
gisle 9b4da98ac4 logs-content: Added escaping of back-ticks (`) to avoid monospace in log posts where a nick containing a back-tick has been copied into the text.
the build was successful
2019-01-05 11:28:22 +01:00
gisle 599ca83f0f logs-content: Fixed quote detection in text post not detecting double-apostrpohes (KVIrc has anti-RP sentiments among their dev team)
the build was successful
2019-01-05 11:19:45 +01:00
gisle b952007f3b logs-content: Fixed character names being omitted on multipart posts where an action post follows a text post.
the build was successful
2019-01-05 11:16:29 +01:00
gisle c0e74f4756 server: Added DNS resolution when setting trust proxy since IPs are temporary in Docker.
the build failed
2019-01-05 11:02:38 +01:00
gisle 68a8e7ef97 story-content: Added hiding of first chapter's title if it matches story name to conserve vertical space.
the build was successful
2019-01-05 10:46:16 +01:00
gisle 5b81f7ff0d data: Fixed unknown nick list sometimes disappearing in character nicks modal.
the build was successful
2018-12-09 12:21:47 +01:00
gisle 78a14ae86b data: Increased limit on change list to 2000. 2018-12-09 12:21:15 +01:00
17 changed files with 141 additions and 21 deletions
@@ -113,6 +113,7 @@ const opText = {
edit: "edited", edit: "edited",
import: "imported", import: "imported",
tag: "tagged", tag: "tagged",
untag: "untagged",
"move-in": "moved in", "move-in": "moved in",
"move-out": "moved-out", "move-out": "moved out",
} }
@@ -1,7 +1,7 @@
import moment from "moment"; import moment from "moment";
<div class="change"> <div class="change">
<span class="change-id color-menu">[${moment(input.data.date).format("MMM D HH:mm:SS")}] * </span> <span class="change-id color-menu">[${moment(input.data.date).format("MMM D HH:mm:ss")}] * </span>
<span class="author color-text">${input.data.author} </span> <span class="author color-text">${input.data.author} </span>
<span class="text color-text">${state.operation} </span> <span class="text color-text">${state.operation} </span>
<span for(link in state.links | status-var=loop) key=link.href class="link"> <span for(link in state.links | status-var=loop) key=link.href class="link">
@@ -10,7 +10,7 @@
</div> </div>
<label>Current Nicks</label> <label>Current Nicks</label>
<div for(nick in input.character.nicks) class="existing-nick-row" > <div for(nick in input.character.nicks) class="existing-nick-row" key=nick >
<div class="content color-text">${nick}</div> <div class="content color-text">${nick}</div>
<button disabled=state.loading on-click("removeNick", nick)>Remove</button> <button disabled=state.loading on-click("removeNick", nick)>Remove</button>
</div> </div>
@@ -13,6 +13,7 @@ module.exports = class {
removed: false, removed: false,
multipart: false, multipart: false,
anchored: false, anchored: false,
prevWasText: false,
} }
this.mounted = false; this.mounted = false;
@@ -48,6 +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.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
this.state.prevWasText = (input.prev.kind === "text");
this.updatePost(input) this.updatePost(input)
} }
@@ -79,14 +81,14 @@ 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, "_").trim(); this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "*").replace(/\`/g, "\\`").trim();
if (this.state.text.startsWith("|")) { if (this.state.text.startsWith("|")) {
this.state.name = "" this.state.name = ""
this.state.text = this.state.text.slice(1).trim(); this.state.text = this.state.text.slice(1).trim();
} }
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("|")) { if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("\''") && !this.state.text.includes("|")) {
const colonIndex = this.state.text.indexOf(": "); const colonIndex = this.state.text.indexOf(": ");
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) { if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) {
this.state.text = this.state.text.replace(": ", ": \"") + "\""; this.state.text = this.state.text.replace(": ", ": \"") + "\"";
@@ -26,7 +26,7 @@
<markdown class="post-content post-scene" source=state.text /> <markdown 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.shortName + state.nameSuffix + " " : ""); $ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : "");
<markdown class="post-content post-action" source=(prefix + state.text) /> <markdown 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">
@@ -49,9 +49,9 @@ module.exports = class {
const search = this.state.search.toLocaleLowerCase() const search = this.state.search.toLocaleLowerCase()
this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name)) this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => compareFilters(a.name, b.name, search))
this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name)) this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => compareFilters(a.name, b.name, search))
this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => a.localeCompare(b)) this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => compareFilters(a, b, search))
} }
updateActive({filter, characters, channels, eventNames}) { updateActive({filter, characters, channels, eventNames}) {
@@ -126,4 +126,29 @@ module.exports = class {
return str.toLowerCase().indexOf(search) !== -1 return str.toLowerCase().indexOf(search) !== -1
} }
}
/**
*
* @param {string} a
* @param {string} b
* @param {string} search
*/
function compareFilters(a, b, search) {
if (search != "") {
if (search.charAt(0) != "#" && a.charAt(0) == "#" && b.charAt(0) == "#") {
search = '#' + search
}
const aStarts = a.toLocaleLowerCase().startsWith(search)
const bStarts = b.toLocaleLowerCase().startsWith(search)
if (aStarts && !bStarts) {
return -1
} else if (bStarts && !aStarts) {
return 1
}
}
return a.localeCompare(b)
} }
+12 -1
View File
@@ -24,11 +24,12 @@ module.exports = class {
} else { } else {
this.state.filter = Object.assign({}, this.state.filter, {search: null}) this.state.filter = Object.assign({}, this.state.filter, {search: null})
} }
} else { } else {
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).filter(f => f !== filter).concat(filter)}) this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).filter(f => f !== filter).concat(filter)})
} }
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
this.updateQuery(this.state.filter) this.updateQuery(this.state.filter)
this.refresh() this.refresh()
@@ -67,6 +68,9 @@ module.exports = class {
if (filter.search) { if (filter.search) {
queries.push("search=" + encodeURIComponent(filter.search)) queries.push("search=" + encodeURIComponent(filter.search))
} }
if (filter.limit) {
queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
}
if (queries.length > 0) { if (queries.length > 0) {
history.replaceState("", "", `/logs/?${queries.join("&")}`) history.replaceState("", "", `/logs/?${queries.join("&")}`)
@@ -75,6 +79,13 @@ module.exports = class {
} }
} }
clearLimit() {
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
this.refresh()
this.updateQuery(this.state.filter)
}
refresh() { refresh() {
logHeaderApi.list(this.state.filter).then(logs => { logHeaderApi.list(this.state.filter).then(logs => {
this.state.logs = logs this.state.logs = logs
+6 -1
View File
@@ -1,8 +1,13 @@
<background src="/assets/images/bg.png" opacity=0.25 /> <background src="/assets/images/bg.png" opacity=0.25 />
<logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters /> <logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
<filter-menu on-add("addFilter") on-remove("removeFilter") filter=state.filter characters=input.characters channels=input.channels eventNames=input.eventNames /> <filter-menu on-add("addFilter") on-remove("removeFilter") filter=state.filter characters=input.characters channels=input.channels eventNames=input.eventNames />
<main class="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)>
<div class="show-more-button" on-click("clearLimit")>
<span>Show All</span>
</div>
</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") />
<import-log-modal enabled=(state.modal === "log.import") on-close("close") /> <import-log-modal enabled=(state.modal === "log.import") on-close("close") />
@@ -0,0 +1,17 @@
main.logs-page {
div.show-more-button {
margin: 1em 0;
padding: 0.5em 2em;
font-size: 2em;
text-align: center;
user-select: none;
color: #DA1;
}
div.show-more-button:hover {
cursor: pointer;
text-decoration: underline;
}
}
@@ -1,6 +1,6 @@
<div class="story-chapter"> <div class="story-chapter">
<a class="anchor" id=input.chapter.id /> <a class="anchor" id=input.chapter.id />
<h1 if(input.chapter.title) class="title color-primary">${input.chapter.title}</h1> <h1 if(input.chapter.title && !input.hideTitle) class="title color-primary">${input.chapter.title}</h1>
<div class="metadata"> <div class="metadata">
<div class="options color-menu"> <div class="options color-menu">
<if-permitted user=input.user author=input.chapter.author permission="chapter.edit"> <if-permitted user=input.user author=input.chapter.author permission="chapter.edit">
@@ -8,18 +8,22 @@
<a on-click("open", "story.edit") class="color-menu">Edit</a> <a on-click("open", "story.edit") class="color-menu">Edit</a>
</if-permitted> </if-permitted>
<if-permitted user=input.user author=state.story.author permission="story.remove"> <if-permitted user=input.user author=state.story.author permission="story.remove">
<a on-click("open", "story.remove") class="color-menu">Remove</a> <a on-click("open", "story.remove") class="color-menu">Remove</a>
</if-permitted> </if-permitted>
</if> </if>
</div> </div>
<story-tags tags=state.story.tags author=state.story.author user=(!state.removed ? input.user : null) on-select("open", "story.tags")/> <story-tags tags=state.story.tags open=state.story.open author=state.story.author user=(!state.removed ? input.user : null) on-select("open", "story.tags")/>
<annotation if(state.removed) level="error"> <annotation if(state.removed) level="error">
<p> <p>
This story has been removed. Your browser has not refreshed the page yet, This story has been removed. Your browser has not refreshed the page yet,
so you can still read it (or back it up) before leaving the page. so you can still read it (or back it up) before leaving the page.
</p> </p>
</annotation> </annotation>
<chapter for(chapter in state.story.chapters) key=chapter.id chapter=chapter user=(!state.removed ? input.user : null) on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id) on-move("moveChapter", chapter) /> <chapter for(chapter in state.story.chapters) key=chapter.id
chapter=chapter
user=(!state.removed ? input.user : null)
hideTitle=(state.story.chapters[0] === chapter && chapter.title === state.story.name)
on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id) on-move("moveChapter", chapter) />
</div> </div>
<create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") /> <create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") />
@@ -1,6 +1,14 @@
<div class="story-tags"> <div class="story-tags">
<a for(tag in input.tags) href=("/story/by-tag/" + tag.kind + "/" + encodeURIComponent(tag.name)) class=["tag", "color-tag-" + tag.kind.toLowerCase()]>${tag.name}</a> <a for(tag in input.tags) href=("/story/by-tag/" + tag.kind + "/" + encodeURIComponent(tag.name)) class=["tag", "color-tag-" + tag.kind.toLowerCase()]>${tag.name}</a>
<if-permitted user=input.user author=input.author permission="story.edit">
<a on-click("select", "edit") class="option color-menu">Manage Tags</a> <if(input.open)>
</if-permitted> <if-permitted user=input.user permission="member">
<a on-click("select", "edit") class="option color-menu">Manage Tags (Open)</a>
</if-permitted>
</if>
<else>
<if-permitted user=input.user author=input.author permission="story.edit">
<a on-click("select", "edit") class="option color-menu">Manage Tags</a>
</if-permitted>
</else>
</div> </div>
+5
View File
@@ -3405,6 +3405,11 @@
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
}, },
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"ipaddr.js": { "ipaddr.js": {
"version": "1.8.0", "version": "1.8.0",
"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",
+1
View File
@@ -26,6 +26,7 @@
"express-session": "^1.15.6", "express-session": "^1.15.6",
"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",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.3.0", "jsonwebtoken": "^8.3.0",
"lasso": "^3.2.6", "lasso": "^3.2.6",
+1 -1
View File
@@ -7,7 +7,7 @@ const changesTemplate = require("../../marko/page/data/changes.marko")
router.get("/", async(req, res) => { router.get("/", async(req, res) => {
res.markoAsync(changesTemplate, { res.markoAsync(changesTemplate, {
changes: changesApi.list(), changes: changesApi.list({limit: 2000}),
selected: {changes: true}, selected: {changes: true},
}) })
}) })
+8 -1
View File
@@ -8,19 +8,26 @@ const {channelApi} = require("../../rpdata/api/Channel")
const listTemplate = require("../../marko/page/logs/list.marko") const listTemplate = require("../../marko/page/logs/list.marko")
router.get("/", async(req, res) => { router.get("/", async(req, res) => {
const filter = {limit: 0} const filter = {limit: 75}
if (req.query.characters) { if (req.query.characters) {
filter.characters = req.query.characters.split(",") filter.characters = req.query.characters.split(",")
filter.limit = 0
} }
if (req.query.channels) { if (req.query.channels) {
filter.channels = req.query.channels.split(",") filter.channels = req.query.channels.split(",")
filter.limit = 0
} }
if (req.query.events) { if (req.query.events) {
filter.events = req.query.events.split(",") filter.events = req.query.events.split(",")
filter.limit = 0
} }
if (req.query.search) { if (req.query.search) {
filter.search = req.query.search filter.search = req.query.search
filter.limit = 0
}
if (req.query.limit) {
filter.limit = parseInt(req.query.limit)
} }
try { try {
+35 -1
View File
@@ -9,6 +9,8 @@ const config = require("./config")
const express = require("express") const express = require("express")
const proxy = require("express-http-proxy") const proxy = require("express-http-proxy")
const proxy2 = require("http-proxy-middleware") const proxy2 = require("http-proxy-middleware")
const ip = require("ip")
const dns = require("dns")
const lasso = require("lasso") const lasso = require("lasso")
const lassoMiddleware = require("lasso/middleware") const lassoMiddleware = require("lasso/middleware")
const markoExpress = require("marko/express") const markoExpress = require("marko/express")
@@ -26,7 +28,39 @@ lasso.configure(require("./marko-config"))
// Set trusted proxy // Set trusted proxy
if (config.trustedProxy != null) { if (config.trustedProxy != null) {
app.set('trust proxy', config.trustedProxy) if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) {
app.set('trust proxy', config.trustedProxy)
console.log("Trusting proxy", config.trustedProxy);
} else {
let trusted = null
setInterval(() => {
dns.resolve4(config.trustedProxy, (err, res) => {
if (err != null) {
if (trusted === null) {
console.error("Failed to resolve trustedProxy hostname")
}
return
}
if (res.length === 0) {
if (trusted === null) {
console.error("No results for trustedProxy hostname")
}
return
}
if (trusted !== res[0]) {
console.log("Trusting proxy", res[0], `(${config.trustedProxy})`);
app.set('trust proxy', res[0])
trusted = res[0]
}
})
}, 1000)
}
} }
// Apply middleware // Apply middleware