Gisle Aune
6 years ago
commit
09f406cba7
84 changed files with 6920 additions and 0 deletions
-
5.babelrc
-
6.gitignore
-
38.jshintrc
-
12.vscode/settings.json
-
39Dockerfile
-
14README.md
-
BINassets/images/bg.png
-
1assets/test.txt
-
47config.js
-
7marko/browser.json
-
51marko/components/background/component.js
-
1marko/components/background/index.marko
-
23marko/components/background/style.less
-
52marko/components/content-list-item/components/item-meta/component.js
-
2marko/components/content-list-item/components/item-meta/index.marko
-
6marko/components/content-list-item/components/item-tag-list/index.marko
-
15marko/components/content-list-item/index.marko
-
51marko/components/content-list-item/style.less
-
5marko/components/content-list/index.marko
-
12marko/components/content-list/style.less
-
10marko/components/layout-navbar/index.marko
-
39marko/components/markdown/component.js
-
3marko/components/markdown/index.marko
-
40marko/components/markdown/plugins/wikilink.js
-
117marko/components/markdown/style.less
-
1marko/components/menu-blurb/index.marko
-
7marko/components/menu-blurb/style.less
-
1marko/components/menu-gap/index.marko
-
3marko/components/menu-gap/style.less
-
1marko/components/menu-header/index.marko
-
5marko/components/menu-header/style.less
-
13marko/components/menu-link/component.js
-
8marko/components/menu-link/index.marko
-
28marko/components/menu-link/style.less
-
12marko/components/menu/index.marko
-
14marko/components/menu/style.less
-
39marko/global/base.less
-
121marko/global/colors.less
-
32marko/global/navbar.less
-
16marko/page/layout.marko
-
13marko/page/logs/components/logs-list/index.marko
-
10marko/page/logs/components/logs-menu/index.marko
-
15marko/page/logs/components/page/component.js
-
5marko/page/logs/components/page/index.marko
-
6marko/page/logs/list.marko
-
52marko/page/story-content/components/chapter-meta/component.js
-
6marko/page/story-content/components/chapter-meta/index.marko
-
20marko/page/story-content/components/chapter-meta/style.less
-
5marko/page/story-content/components/chapter/component.js
-
10marko/page/story-content/components/chapter/index.marko
-
14marko/page/story-content/components/chapter/style.less
-
8marko/page/story-content/components/page/index.marko
-
9marko/page/story-content/components/page/style.less
-
40marko/page/story-content/components/story-content-menu/component.js
-
9marko/page/story-content/components/story-content-menu/index.marko
-
3marko/page/story-content/components/story-tags/index.marko
-
20marko/page/story-content/components/story-tags/style.less
-
6marko/page/story-content/view.marko
-
11marko/page/story/components/story-list/index.marko
-
11marko/page/story/components/story-menu/index.marko
-
9marko/page/story/list.marko
-
8marko/page/story/tag-list.marko
-
29middleware/locals.js
-
29middleware/passport.js
-
15middleware/session.js
-
4922package-lock.json
-
41package.json
-
46routes/auth.js
-
56routes/graphql.js
-
19routes/logs/index.js
-
13routes/story-content/index.js
-
22routes/story/by-category.js
-
24routes/story/by-tag.js
-
7routes/story/common.js
-
20routes/story/index.js
-
20routes/story/tag-list.js
-
53rpdata/api/Chapter.js
-
5rpdata/api/Character.js
-
84rpdata/api/LogHeader.js
-
147rpdata/api/Story.js
-
34rpdata/api/Tag.js
-
6rpdata/api/index.js
-
40rpdata/client.js
-
101server.js
@ -0,0 +1,5 @@ |
|||
{ |
|||
"presets": [ |
|||
"es2015" |
|||
] |
|||
} |
@ -0,0 +1,6 @@ |
|||
*.marko.js |
|||
/node_modules |
|||
npm-debug.log |
|||
/.cache |
|||
/node_modules |
|||
/.static |
@ -0,0 +1,38 @@ |
|||
{ |
|||
"predef": [ |
|||
|
|||
], |
|||
|
|||
"globals": { |
|||
"define": true, |
|||
"require": true |
|||
}, |
|||
|
|||
"node" : true, |
|||
"es5" : false, |
|||
"browser" : true, |
|||
"boss" : false, |
|||
"curly": false, |
|||
"debug": false, |
|||
"devel": false, |
|||
"evil": true, |
|||
"forin": false, |
|||
"immed": true, |
|||
"laxbreak": false, |
|||
"newcap": true, |
|||
"noarg": true, |
|||
"noempty": false, |
|||
"nonew": true, |
|||
"nomen": false, |
|||
"onevar": false, |
|||
"plusplus": false, |
|||
"regexp": false, |
|||
"undef": true, |
|||
"sub": false, |
|||
"white": false, |
|||
"eqeqeq": true, |
|||
"latedef": true, |
|||
"unused": "vars", |
|||
"strict": false, |
|||
"eqnull": true |
|||
} |
@ -0,0 +1,12 @@ |
|||
{ |
|||
"files.exclude": { |
|||
"**/**/**/**/*.marko.js": true, |
|||
"**/**/**/*.marko.js": true, |
|||
"**/**/*.marko.js": true, |
|||
"**/*.marko.js": true, |
|||
"node_modules": true, |
|||
".cache": true, |
|||
".static": true, |
|||
}, |
|||
"material-icon-theme.folders.theme": "classic" |
|||
} |
@ -0,0 +1,39 @@ |
|||
## 1. Builder |
|||
# Use nodejs 10 |
|||
FROM node:10-alpine AS builder |
|||
|
|||
# Load repository into container |
|||
WORKDIR /rpdata-frontend |
|||
COPY . . |
|||
|
|||
# Setup development environment |
|||
RUN apk add --no-cache git |
|||
|
|||
# Erease all traces of browser-refresh |
|||
RUN sed -i '/browser-refresh/d' ./marko/page/layout.marko |
|||
RUN sed -i '/browser-refresh/d' ./package.json |
|||
|
|||
# Install and compress node_modules |
|||
RUN rm -rf node_modules |
|||
RUN mkdir node_modules |
|||
RUN npm install -g modclean |
|||
RUN npm install |
|||
RUN npm dedupe |
|||
RUN modclean -r |
|||
|
|||
# Remove dev stuff |
|||
RUN rm -rf .static |
|||
RUN rm -rf .cache |
|||
RUN rm -rf .git |
|||
|
|||
## 2. Regroup |
|||
FROM node:10-alpine |
|||
RUN apk add --no-cache ca-certificates |
|||
|
|||
# Bring data over from builder |
|||
WORKDIR /rpdata-frontend |
|||
COPY --from=builder /rpdata-frontend /rpdata-frontend |
|||
ENV NODE_ENV=production |
|||
|
|||
# Entry point |
|||
CMD ["/usr/local/bin/node", "server"] |
@ -0,0 +1,14 @@ |
|||
# Sample App: Marko + Express |
|||
|
|||
This is the [official sample](https://github.com/marko-js-samples/marko-express) changed to get it working with the latest versions of lasso, as well as some personal preferences. |
|||
|
|||
A lot of compiled/cached files are hidden in Visual Studio Code, see the config file in the `.vscode` folder to change that. |
|||
|
|||
## Install |
|||
|
|||
```sh |
|||
npm install |
|||
npm run dev |
|||
``` |
|||
|
|||
Then open the URL printed in the console. |
After Width: 301 | Height: 256 | Size: 62 KiB |
@ -0,0 +1 @@ |
|||
Hello |
@ -0,0 +1,47 @@ |
|||
const fs = require("fs") |
|||
|
|||
const config = { |
|||
graphqlEndpoint: "http://127.0.0.1:17000/graphql", |
|||
port: 8080, |
|||
root: "http://localhost:8080", |
|||
auth0: { |
|||
secret: "INVALID_SECRET", |
|||
clientId: "", |
|||
domain: "aiterp.eu.auth0.com", |
|||
}, |
|||
session: { |
|||
secret: "", |
|||
secure: false, |
|||
}, |
|||
redis: { |
|||
host: "127.0.0.1", |
|||
port: 6379, |
|||
}, |
|||
backend: { |
|||
kid: "", |
|||
secret: "", |
|||
} |
|||
} |
|||
|
|||
if (typeof(window) !== "undefined") { |
|||
config.graphqlEndpoint = "/graphql" |
|||
|
|||
delete config.port |
|||
delete config.root |
|||
delete config.auth0 |
|||
delete config.session |
|||
delete config.redis |
|||
delete config.backend |
|||
} else { |
|||
try { |
|||
const data = fs.readFileSync("/etc/aiterp/rpdata-frontend.json", "utf8") |
|||
const parsed = JSON.parse(data) |
|||
if (typeof(parsed) == "object" && !Array.isArray(parsed)) { |
|||
Object.assign(config, parsed) |
|||
} |
|||
} catch(err) { |
|||
console.error("Failed to load configuration: " + err.message || err.stack || err) |
|||
} |
|||
} |
|||
|
|||
module.exports = config |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"dependencies": [ |
|||
"./global/base.less", |
|||
"./global/colors.less", |
|||
"./global/navbar.less" |
|||
] |
|||
} |
@ -0,0 +1,51 @@ |
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
src: input.src, |
|||
orientation: "portrait", |
|||
style: { |
|||
filter: `opacity(${input.opacity || 0.20})` |
|||
} |
|||
} |
|||
} |
|||
|
|||
onInput(input) { |
|||
if (input.src != this.state.src) { |
|||
this.updateOrientation() |
|||
} |
|||
|
|||
if (input.opacity != this.state.opacity) { |
|||
this.state.style.filter = `opacity(${input.opacity || 0.20})` |
|||
} |
|||
} |
|||
|
|||
onMount() { |
|||
this.updateOrientation() |
|||
} |
|||
|
|||
updateOrientation() { |
|||
const image = this.getEl("image", 0) |
|||
|
|||
if (image.complete && typeof(image.naturalHeight) !== 'undefined' && image.naturalHeight !== 0) { |
|||
const width = image.naturalWidth |
|||
const height = image.naturalHeight |
|||
|
|||
this.setLandscape(width > (height * 1.50)) |
|||
} else { |
|||
image.onload = () => { |
|||
const width = image.naturalWidth |
|||
const height = image.naturalHeight |
|||
|
|||
this.setLandscape(width > (height * 1.50)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
setLandscape(value) { |
|||
const orientation = value ? "landscape" : "portrait" |
|||
|
|||
if (orientation !== this.state.orientation) { |
|||
this.state.orientation = orientation |
|||
} |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
<img key="image" src=input.src style=state.style class=["background", state.orientation] /> |
@ -0,0 +1,23 @@ |
|||
.background { |
|||
/* Set rules to fill background */ |
|||
width: 100%; |
|||
/* Set up proportionate scaling */ |
|||
min-height: 100%; |
|||
height: auto; |
|||
/* Set up positioning */ |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
user-select: none; |
|||
-webkit-user-drag: none; |
|||
|
|||
z-index: -1; |
|||
} |
|||
|
|||
.background.landscape { |
|||
/* Set rules to fill background */ |
|||
height: 100%; |
|||
/* Set up proportionate scaling */ |
|||
min-width: 100%; |
|||
width: auto; |
|||
} |
@ -0,0 +1,52 @@ |
|||
const moment = require("moment") |
|||
|
|||
module.exports = class { |
|||
onCreate({kind, value, updated}) { |
|||
this.state = {text: null, color: "color-menu", tooltip: null} |
|||
|
|||
if (value == null || value == "") { |
|||
return |
|||
} |
|||
|
|||
switch (kind) { |
|||
case "date": { |
|||
const m = moment(value) |
|||
if (m.year() < 2) { |
|||
break |
|||
} |
|||
|
|||
const dateStr = m.format("MMM D, YYYY") |
|||
|
|||
this.state.text = dateStr |
|||
|
|||
if (updated != null && Date.parse(updated) > Date.parse(value)) { |
|||
const m2 = moment(updated) |
|||
this.state.tooltip = "Originally posted: " + dateStr |
|||
this.state.text = m2.format("MMM D, YYYY") + " *" |
|||
} |
|||
|
|||
break |
|||
} |
|||
|
|||
case "tag": { |
|||
this.state.color = `color-tag-${value.kind.toLowerCase()}` |
|||
this.state.text = value.name |
|||
|
|||
break |
|||
} |
|||
|
|||
case "author": { |
|||
this.state.text = value |
|||
|
|||
break |
|||
} |
|||
|
|||
default: { |
|||
this.state.color = "color-menu" |
|||
this.state.text = value |
|||
|
|||
break |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
<div if(state.text != null && state.text != "") class=["item-meta", input.kind, state.color] title=state.tooltip>${state.text}</div> |
|||
<div class="spc"> </div> |
@ -0,0 +1,6 @@ |
|||
<if(input.enableAllTags)> |
|||
<item-meta for(tag in input.tags) kind="tag" value=tag /> |
|||
</if> |
|||
<else> |
|||
<item-meta kind="tag" value=(input.tags[0] || null) /> |
|||
</else> |
@ -0,0 +1,15 @@ |
|||
<tr class="content-list-item color-primary"> |
|||
<td class="icon color-highlight-primary">${input.icon}</td> |
|||
<td class="content color-primary color-highlight-dark"> |
|||
<div class="title"> |
|||
<a class="color-primary" href=input.link>${input.name}</a> |
|||
</div> |
|||
<div class="description color-text">${input.description || ""}</div> |
|||
<div class="meta"> |
|||
<item-meta kind="date" value=input.createdDate updated=input.updatedDate /> |
|||
<item-meta kind="date" value=input.fictionalDate /> |
|||
<item-tag-list tags=input.tags enableAllTags=input.enableAllTags /> |
|||
<item-meta kind="author" value=input.author /> |
|||
</div> |
|||
</td> |
|||
</tr> |
@ -0,0 +1,51 @@ |
|||
tr.content-list-item { |
|||
user-select: none; |
|||
outline: 0.05px solid; |
|||
|
|||
> td.icon { |
|||
font-size: 2.75em; |
|||
text-align: center; |
|||
width: 2.5ch; |
|||
outline: 0.05px solid; |
|||
} |
|||
|
|||
> td.content { |
|||
padding: 0.25em; |
|||
padding-bottom: 0.33em; |
|||
margin: 0; |
|||
|
|||
> .title { |
|||
padding: 0.25em 1ch; |
|||
user-select: text; |
|||
|
|||
a { |
|||
font-size: 1.333em; |
|||
} |
|||
a:hover { |
|||
color: white; |
|||
} |
|||
} |
|||
|
|||
> .description { |
|||
margin: 0 1ch; |
|||
|
|||
p { |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
> .meta { |
|||
> div { |
|||
display: inline-block; |
|||
padding: 0.25em 1ch; |
|||
|
|||
user-select: text; |
|||
} |
|||
|
|||
> div.spc { |
|||
padding: 0; |
|||
user-select: none; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
<table class="content-list" cellSpacing="0px"> |
|||
<tbody> |
|||
<include(input.renderBody)/> |
|||
</tbody> |
|||
</table> |
@ -0,0 +1,12 @@ |
|||
table.content-list { |
|||
padding-bottom: 3em; |
|||
|
|||
width: 99%; |
|||
max-width: 100ch; |
|||
padding-right: 1em; |
|||
|
|||
margin: auto; |
|||
|
|||
border-collapse: separate; |
|||
border-spacing: 0 1em; |
|||
} |
@ -0,0 +1,10 @@ |
|||
<!-- style in marko/global/navbar.less --> |
|||
|
|||
<nav class="layout-navbar color-primary"> |
|||
<h1 class="color-primary">Aite RP</h1> |
|||
<div class="vr color-menu" /> |
|||
<div class="options"> |
|||
<a class=(input.site === "story" ? "color-primary" : "color-menu") href="/story/">Story</a> |
|||
<a class=(input.site === "logs" ? "color-primary" : "color-menu") href="/logs/">Logs</a> |
|||
</div> |
|||
</nav> |
@ -0,0 +1,39 @@ |
|||
const MarkdownIt = require("markdown-it") |
|||
const md = new MarkdownIt({ |
|||
html: true, |
|||
linkify: true, |
|||
typographer: false, |
|||
langPrefix: "language-", |
|||
}) |
|||
md.use(require("markdown-it-container"), "green") |
|||
md.use(require("markdown-it-container"), "orange") |
|||
md.use(require("markdown-it-container"), "blue") |
|||
md.use(require("./plugins/wikilink")) |
|||
|
|||
const renderer = md.renderer |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
render: () => {}, |
|||
} |
|||
|
|||
this.render(input.source || "") |
|||
} |
|||
|
|||
onInput(input) { |
|||
if (input.source) { |
|||
this.render(input.source) |
|||
} |
|||
} |
|||
|
|||
render(source) { |
|||
this.state.render = (out) => { |
|||
if (out.w) { |
|||
out.w(md.render(source)) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
renderer.rules |
@ -0,0 +1,3 @@ |
|||
<div class=["markdown-content", "color-text", input.class]> |
|||
<include(state.render) /> |
|||
</div> |
@ -0,0 +1,40 @@ |
|||
/* |
|||
|
|||
Based on the markdown-it-wikilinks, which is distributed under the MIT license. |
|||
|
|||
https://github.com/jsepia/markdown-it-wikilinks @ 79fe609a03917f7df11128231118a66a559076d0
|
|||
|
|||
Copyright (c) 2016 Julio Sepia |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
|
|||
*/ |
|||
|
|||
const Plugin = require('markdown-it-regexp') |
|||
|
|||
module.exports = new Plugin(/\[\[([\w'\s/]+)(\|([\w'\s/]+))?\]\]/, match => { |
|||
let label = "" |
|||
let page = "" |
|||
|
|||
if (match[3] != null) { |
|||
label = match[3] |
|||
page = encodeURIComponent(match[1]) |
|||
} else { |
|||
label = match[1] |
|||
page = encodeURIComponent(label) |
|||
} |
|||
|
|||
if (label == "" || page == "") { |
|||
return match.input |
|||
} |
|||
|
|||
return `<a class="wiki-link" href="https://wiki.aiterp.net?title=${page}">${label}</a>` |
|||
} |
|||
) |
|||
|
@ -0,0 +1,117 @@ |
|||
div.markdown-content { |
|||
line-height: 1.333em; |
|||
font-size: 1.25em; |
|||
|
|||
h1 { |
|||
font-size: 2em; |
|||
margin: 0; |
|||
margin-top: 0.75em; |
|||
line-height: 1.2em; |
|||
text-align: left; |
|||
|
|||
border-bottom: 1px solid; |
|||
} |
|||
h1:first-child { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
h2 { |
|||
font-size: 1.5em; |
|||
margin: 0; |
|||
margin-top: 0.75em; |
|||
padding-bottom: 0.25em; |
|||
|
|||
border-bottom: 1px solid; |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 1.25em; |
|||
margin: 0; |
|||
margin-top: 1em; |
|||
padding: 0; |
|||
margin-bottom: -0.25em; |
|||
} |
|||
|
|||
h4 { |
|||
font-size: 1em; |
|||
margin-top: 1em; |
|||
margin-bottom: -0.5em; |
|||
padding: 0; |
|||
} |
|||
|
|||
h5 { |
|||
font-size: 1em; |
|||
margin: 0; |
|||
margin-bottom: -0.5em; |
|||
} |
|||
|
|||
h6 { |
|||
font-size: 1em; |
|||
opacity: 0.5; |
|||
margin: 0; |
|||
margin-bottom: -0.5em; |
|||
} |
|||
|
|||
hr { |
|||
border: none; |
|||
margin: 2em 2ch; |
|||
border-bottom: 4px dotted; |
|||
opacity: 0.75; |
|||
} |
|||
|
|||
ul, ol { |
|||
padding: 0.5em 3ch; |
|||
margin: 0; |
|||
} |
|||
|
|||
p { |
|||
padding: 0.5em 0; |
|||
margin: 0; |
|||
} |
|||
p:first-child { |
|||
padding-top: 0.25em; |
|||
} |
|||
|
|||
.orange, .blue, .green, blockquote { |
|||
h1, h2, h3, h4, h5, h6 { |
|||
margin: 0; |
|||
padding: 0.5em 0; |
|||
} |
|||
|
|||
margin: 0.5em 0; |
|||
padding: 0.125em 0.5em 0 0.5em; |
|||
outline: 1px solid; |
|||
|
|||
p { |
|||
padding: 0.5em 0; |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
pre { |
|||
border: 1px solid; |
|||
padding: 0.5em; |
|||
} |
|||
|
|||
table { |
|||
width: 100%; |
|||
border: 1px solid; |
|||
padding: 0em 0.5em; |
|||
|
|||
th { |
|||
font-size: 0.75em; |
|||
padding: 0 1ch; |
|||
} |
|||
} |
|||
|
|||
img { |
|||
width: 100%; |
|||
} |
|||
|
|||
a { |
|||
opacity: 0.75; |
|||
} |
|||
a:hover { |
|||
opacity: 1; |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
<div class=["menu-blurb", input.class]><include(input.renderBody)/></div> |
@ -0,0 +1,7 @@ |
|||
.menu-blurb { |
|||
opacity: 0.5; |
|||
padding: 0.25em 1.5ch; |
|||
} |
|||
.menu-blurb.error { |
|||
color: red; |
|||
} |
@ -0,0 +1 @@ |
|||
<div class="menu-gap"></div> |
@ -0,0 +1,3 @@ |
|||
.menu-gap { |
|||
padding: 0.25em; |
|||
} |
@ -0,0 +1 @@ |
|||
<div class="menu-header color-menu"><include(input.renderBody)/></div> |
@ -0,0 +1,5 @@ |
|||
.menu-header { |
|||
padding: 0.125em 1.5ch; |
|||
opacity: 0.5; |
|||
user-select: none; |
|||
} |
@ -0,0 +1,13 @@ |
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
classes: ["menu-link", "color-menu", input.class], |
|||
} |
|||
|
|||
this.state.classes.push(input.selected ? "selected" : "not-selected") |
|||
} |
|||
|
|||
onClick() { |
|||
this.emit("click") |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
<a on-click("onClick") href=input.href> |
|||
<div class=state.classes> |
|||
<div class="icon color-primary">${input.icon}</div> |
|||
<div class=["text", input.selected ? "color-primary" : input.textClass]> |
|||
<include(input.renderBody) /> |
|||
</div> |
|||
</div> |
|||
</a> |
@ -0,0 +1,28 @@ |
|||
div.menu-link { |
|||
width: 100%; |
|||
padding-left: 0.5ch; |
|||
font-size: 1.1em; |
|||
|
|||
cursor: pointer; |
|||
|
|||
div.icon { |
|||
display: inline-block; |
|||
width: 3ch; |
|||
padding: 0.15em 0.125em; |
|||
text-align: center; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
div.text { |
|||
display: inline-block; |
|||
max-width: 22ch; |
|||
white-space: nowrap; |
|||
overflow-x: hidden; |
|||
text-overflow: ellipsis; |
|||
vertical-align: middle; |
|||
|
|||
span.weak { |
|||
opacity: 0.3333; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,12 @@ |
|||
<div class="menu"> |
|||
<include(input.renderBody) /> |
|||
<if(input.user != null)> |
|||
<menu-gap /> |
|||
<menu-header>User</menu-header> |
|||
<menu-link if(!input.user.loggedIn) icon="L" href="/auth/login">Login</menu-link> |
|||
<menu-link if(input.user.loggedIn) icon="L" href="/auth/logout">Logout <span class="weak">(${input.user.name})</span></menu-link> |
|||
</if> |
|||
<else-if(!input.allowNoUser)> |
|||
<menu-blurb class="error">input.user is missing!</menu-blurb> |
|||
</else-if> |
|||
</div> |
@ -0,0 +1,14 @@ |
|||
.menu { |
|||
position: fixed; |
|||
top: 5.5em; |
|||
bottom: 0; |
|||
width: 28ch; |
|||
|
|||
padding: 0.333em; |
|||
|
|||
text-align: left; |
|||
|
|||
user-select: none; |
|||
overflow-x: hidden; |
|||
overflow-y: auto; |
|||
} |
@ -0,0 +1,39 @@ |
|||
body { |
|||
margin: 0; |
|||
padding: 0; |
|||
font-family: sans-serif; |
|||
background-color: black; |
|||
color: white; |
|||
|
|||
main { |
|||
margin-left: 30ch; // See side-menu |
|||
width: calc(100% - 30ch); |
|||
margin-right: 0; |
|||
} |
|||
|
|||
a { |
|||
text-decoration: none; |
|||
} |
|||
} |
|||
|
|||
/* |
|||
* Less glaring chrome/ium scrollbars. |
|||
*/ |
|||
::-webkit-scrollbar { |
|||
width: 2px; |
|||
/* for vertical scrollbars */ |
|||
height: 2px; |
|||
/* for horizontal scrollbars */ |
|||
} |
|||
::-webkit-scrollbar-button { |
|||
background-color: #555; |
|||
color: 000; |
|||
display: none; |
|||
} |
|||
::-webkit-scrollbar-track { |
|||
background-color: #222; |
|||
} |
|||
::-webkit-scrollbar-thumb { |
|||
background-color: #555; |
|||
color: #000; |
|||
} |
@ -0,0 +1,121 @@ |
|||
body.theme-story { |
|||
.color-menu { |
|||
color: #789; |
|||
} |
|||
.menu-link.selected { |
|||
background-color: rgba(17, 187, 255, 0.125); |
|||
} |
|||
.menu-link:hover, .menu > .head-menu > a:hover { |
|||
background-color: rgba(17, 187, 255, 0.25); |
|||
} |
|||
.color-primary, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a { |
|||
color: #4BF; |
|||
} |
|||
.color-text { |
|||
color: #ABC; |
|||
} |
|||
.color-highlight-primary { |
|||
background-color: rgba(34, 187, 255, 0.25); |
|||
color: #4BF; |
|||
} |
|||
.color-highlight-dark { |
|||
background-color: rgba(0, 9, 18, 0.50); |
|||
} |
|||
} |
|||
|
|||
body.theme-logs { |
|||
.color-menu { |
|||
color: #776; |
|||
} |
|||
.menu-link.selected { |
|||
background-color: rgba(255, 187, 17, 0.125); |
|||
} |
|||
.menu-link:hover, .Menu > .head-menu > a:hover { |
|||
background-color: rgba(255, 187, 17, 0.25); |
|||
} |
|||
.color-primary, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a { |
|||
color: #DA1; |
|||
} |
|||
.color-text { |
|||
color: #AAA; |
|||
} |
|||
.color-highlight-dark { |
|||
background-color: rgba(18, 9, 0, 0.50); |
|||
} |
|||
.color-highlight-primary { |
|||
background-color: rgba(255, 187, 15, 0.25); |
|||
color: #DA1; |
|||
} |
|||
} |
|||
|
|||
.theme-mapp { |
|||
.color-menu { |
|||
color: #898; |
|||
} |
|||
.menu-link.selected { |
|||
background-color: rgba(35, 255, 128, 0.125); |
|||
} |
|||
.menu-link:hover, .menu > .head-menu > a:hover { |
|||
background-color: rgba(35, 255, 128, 0.25); |
|||
} |
|||
.color-primary, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a { |
|||
color: #1F8; |
|||
} |
|||
.color-text { |
|||
color: #ACB; |
|||
} |
|||
} |
|||
|
|||
div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, body.theme-logs div.markdown-content table, body.theme-logs div.markdown-content pre { |
|||
background-color: rgba(221, 170, 17, 0.125); |
|||
color: #DA1; |
|||
|
|||
h1, h2, h3 { |
|||
color: #DA1; |
|||
} |
|||
} |
|||
div.markdown-content .blue, body.theme-story div.markdown-content blockquote, body.theme-story div.markdown-content table, body.theme-story div.markdown-content pre { |
|||
background-color: rgba(34, 187, 255, 0.125); |
|||
color: #4BF; |
|||
|
|||
h1, h2, h3 { |
|||
color: #4BF; // |
|||
} |
|||
} |
|||
div.markdown-content .green, body.theme-mapp div.markdown-content blockquote, body.theme-mapp div.markdown-content table, body.theme-mapp div.markdown-content pre { |
|||
background-color: rgba(17,255,136, 0.125); |
|||
color: #1F8; |
|||
|
|||
h1, h2, h3 { |
|||
color: #1F8; |
|||
} |
|||
} |
|||
|
|||
/* Tag colors */ |
|||
.color-tag-event { |
|||
color: #B77; |
|||
} |
|||
.color-tag-location { |
|||
color: #7B7; |
|||
} |
|||
.color-tag-organization { |
|||
color: #B7B; |
|||
} |
|||
.color-tag-series { |
|||
color: #BB7; |
|||
} |
|||
.color-tag-character { |
|||
color: #37B; |
|||
} |
|||
|
|||
/* Danger */ |
|||
.color-danger { |
|||
color: #FA3; |
|||
} |
|||
.color-error { |
|||
color: #D20; |
|||
} |
|||
|
|||
.menu-link:hover, .menu-link:hover .text, .menu > .head-menu > a:hover { |
|||
color: #EEE !important; |
|||
} |
@ -0,0 +1,32 @@ |
|||
nav.layout-navbar { |
|||
position: fixed; |
|||
top: 0; |
|||
bottom: 0; |
|||
width: 30ch; |
|||
text-align: center; |
|||
|
|||
user-select: none; |
|||
|
|||
h1 { |
|||
display: inline-block; |
|||
font-size: 2em; |
|||
padding: 0.5em 0 0 0; |
|||
margin: 0; |
|||
} |
|||
|
|||
.options { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
|
|||
a { |
|||
display: inline-block; |
|||
padding: 0.25em; |
|||
margin: 0 0.25ch; |
|||
opacity: 0.666; |
|||
} |
|||
a:hover { |
|||
opacity: 1; |
|||
color: white; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
<lasso-page package-path="../browser.json"/> |
|||
|
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"/> |
|||
<title>${input.title ? `${input.title} - Aite RP` : "Aite RP"}</title> |
|||
<lasso-head/> |
|||
</head> |
|||
<body style={backgroundColor: "black"} class=("theme-" + input.site)> |
|||
<layout-navbar site=input.site /> |
|||
<include(input.body) /> |
|||
<lasso-body/> |
|||
<browser-refresh/> |
|||
</body> |
|||
</html> |
@ -0,0 +1,13 @@ |
|||
import moment from "moment" |
|||
|
|||
<content-list> |
|||
<content-list-item for(log in input.logs) |
|||
key=log.shortId |
|||
icon="L" |
|||
link=("/logs/" + log.id) |
|||
description=log.description |
|||
name=(log.title || (log.channelName + " - " + moment(log.date).format("MMMM D, YYYY"))) |
|||
tags=[{kind: "Location", name: log.channelName}, {kind: "Event", name: log.event}] |
|||
enableAllTags=true |
|||
createdDate=log.date /> |
|||
</content-list> |
@ -0,0 +1,10 @@ |
|||
<menu user=input.user> |
|||
<menu-header>Logs</menu-header> |
|||
<menu-link selected=input.selected.index icon="L" href="/logs/">All</menu-link> |
|||
<menu-gap /> |
|||
<menu-header>Filters</menu-header> |
|||
<menu-gap /> |
|||
<menu-header>Data</menu-header> |
|||
<menu-link selected=input.selected.channels icon="C" href="/logs/characters/">Characters</menu-link> |
|||
<menu-link selected=input.selected.channels icon="#" href="/logs/channels">Channels</menu-link> |
|||
</menu> |
@ -0,0 +1,15 @@ |
|||
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader") |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
logs: input.logs |
|||
} |
|||
} |
|||
|
|||
onMount() { |
|||
logHeaderApi.list({limit: 0}).then(logs => { |
|||
this.state.logs = logs |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
<background src="/assets/images/bg.png" opacity=0.25 /> |
|||
<logs-menu key="menu" selected=(input.selected || {}) user=input.user /> |
|||
<main> |
|||
<logs-list logs=state.logs /> |
|||
</main> |
@ -0,0 +1,6 @@ |
|||
<include("../layout", {title: "Logs", site: "logs"})> |
|||
<@body> |
|||
<!-- Page needed to get component.js functionality --> |
|||
<page logs=input.logs user=input.user selected=input.selected /> |
|||
</@body> |
|||
</include> |
@ -0,0 +1,52 @@ |
|||
const moment = require("moment") |
|||
|
|||
module.exports = class { |
|||
onCreate({kind, weak, value, updated}) { |
|||
this.state = {text: null, color: "color-menu", href: null, tooltip: null} |
|||
|
|||
if (value == null || value == "") { |
|||
return |
|||
} |
|||
|
|||
switch (kind) { |
|||
case "date": { |
|||
const m = moment(value) |
|||
if (m.year() < 2) { |
|||
break |
|||
} |
|||
|
|||
if (!weak) { |
|||
this.state.tooltip = `See all stories set during ${m.format("MMMM YYYY")}` |
|||
this.state.href = `/story/by-month/${m.format("YYYY-MM")}/` |
|||
} |
|||
|
|||
const dateStr = m.format("MMM D, YYYY") |
|||
|
|||
this.state.text = dateStr |
|||
|
|||
if (updated != null && Date.parse(updated) > Date.parse(value)) { |
|||
const m2 = moment(updated) |
|||
this.state.tooltip = "Originally posted: " + dateStr |
|||
this.state.text = m2.format("MMM D, YYYY") + " *" |
|||
} |
|||
|
|||
break |
|||
} |
|||
|
|||
case "author": { |
|||
this.state.text = value |
|||
this.state.tooltip = "See all stories by " + value |
|||
this.state.href = `/story/by-author/${value}/` |
|||
|
|||
break |
|||
} |
|||
|
|||
default: { |
|||
this.state.color = "color-menu" |
|||
this.state.text = value |
|||
|
|||
break |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
<if(state.text != null && state.text != "")> |
|||
<div class=["chapter-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip> |
|||
<a if(state.href != null) href=state.href>${state.text}</a> |
|||
<span else>${state.text}</span> |
|||
</div> |
|||
</if> |
@ -0,0 +1,20 @@ |
|||
div.chapter-meta { |
|||
display: inline-block; |
|||
padding: 0; |
|||
padding-right: 1em; |
|||
margin: 0; |
|||
|
|||
user-select: none; |
|||
|
|||
a { |
|||
color: inherit; |
|||
border-bottom: 0.25px dotted; |
|||
} |
|||
a:hover { |
|||
border-bottom: 0.5px solid; |
|||
} |
|||
} |
|||
|
|||
div.chapter-meta.weak { |
|||
opacity: 0.5; |
|||
} |
@ -0,0 +1,5 @@ |
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = Object.assign({}, input.chapter) |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
<div class="story-chapter"> |
|||
<a class="anchor" id=state.id /> |
|||
<div class="metadata"> |
|||
<chapter-meta kind="title" value=state.title /> |
|||
<chapter-meta kind="date" value=state.fictionalDate /> |
|||
<chapter-meta weak kind="date" value=state.createdDate /> |
|||
<chapter-meta weak kind="author" value=state.author /> |
|||
</div> |
|||
<markdown class="chapter-content" source=state.source /> |
|||
</div> |
@ -0,0 +1,14 @@ |
|||
.story-chapter { |
|||
padding-bottom: 1em; |
|||
|
|||
.metadata { |
|||
padding: 0; |
|||
margin: 0; |
|||
} |
|||
|
|||
a.anchor { |
|||
display: block; |
|||
position: relative; |
|||
top: -8em; |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
<story-content-menu story=input.story selected=(input.selected || {}) user=input.user /> |
|||
<main> |
|||
<div class="story-content"> |
|||
<h1 class="color-primary">${input.story.name}</h1> |
|||
<story-tags tags=input.story.tags /> |
|||
<chapter for(chapter in input.story.chapters) key=chapter.id chapter=chapter /> |
|||
</div> |
|||
</main> |
@ -0,0 +1,9 @@ |
|||
.story-content { |
|||
width: 90%; |
|||
max-width: 75ch; |
|||
margin: auto; |
|||
|
|||
h1 { |
|||
text-align: center; |
|||
} |
|||
} |
@ -0,0 +1,40 @@ |
|||
const moment = require("moment") |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
chapterTitles: {}, |
|||
fdateLink: "", |
|||
fdate: "", |
|||
selectedChapter: "" |
|||
} |
|||
} |
|||
|
|||
onMount() { |
|||
this.state.selectedChapter = (window.location.hash||"#").slice(1) |
|||
} |
|||
|
|||
onInput(input) { |
|||
if (input.story.fictionalDate && input.story.fictionalDate.getUTCFullYear() > 10) { |
|||
const fdate = input.story.fictionalDate |
|||
|
|||
this.state.fdateLink = `/story/by-month/${moment(fdate).format("YYYY-MM")}` |
|||
this.state.fdate = moment(fdate).format("MMM D, YYYY") |
|||
} |
|||
|
|||
for (const chapter of input.story.chapters) { |
|||
let title = "Chapter " + input.story.chapters.indexOf(chapter) + 1 |
|||
if (chapter.title) { |
|||
title = chapter.title |
|||
} else if (chapter.fictionalDate && chapter.fictionalDate.getUTCFullYear() > 1) { |
|||
title = moment(chapter.fictionalDate).format("MMM D, YYYY") |
|||
} |
|||
|
|||
this.state.chapterTitles[chapter.id] = title |
|||
} |
|||
} |
|||
|
|||
selectChapter(id) { |
|||
this.state.selectedChapter = id |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
<menu user=input.user> |
|||
<if(input.story.chapters.length > 1)> |
|||
<menu-header>${input.story.name}</menu-header> |
|||
<for(chapter in input.story.chapters | status-var=loop)> |
|||
<menu-link on-click("selectChapter", chapter.id) href=("#" + chapter.id) selected=(state.selectedChapter === chapter.id) icon=(loop.getIndex()+1)>${state.chapterTitles[chapter.id]}</menu-link> |
|||
</for> |
|||
<menu-gap /> |
|||
</if> |
|||
</menu> |
@ -0,0 +1,3 @@ |
|||
<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> |
|||
</div> |
@ -0,0 +1,20 @@ |
|||
div.story-tags { |
|||
text-align: center; |
|||
margin-bottom: 1em; |
|||
margin-top: -1em; |
|||
|
|||
a.tag { |
|||
display: inline-block; |
|||
margin: 0.5em 0.5ch; |
|||
padding: 0.0625em 0.5ch; |
|||
|
|||
outline: 1px solid; |
|||
opacity: 0.75; |
|||
|
|||
user-select: none; |
|||
} |
|||
a.tag:hover { |
|||
opacity: 1; |
|||
cursor: pointer; |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
<include("../layout", {title: "Stories", site: "story"})> |
|||
<@body> |
|||
<background src="/assets/images/bg.png" opacity=0.25 /> |
|||
<page story=input.story user=input.user selected=input.selected /> |
|||
</@body> |
|||
</include> |
@ -0,0 +1,11 @@ |
|||
<content-list> |
|||
<content-list-item for(story in input.stories) |
|||
icon=story.category.charAt(0) |
|||
link=("/story/" + story.id) |
|||
name=story.name |
|||
createdDate=story.createdDate |
|||
fictionalDate=story.fictionalDate |
|||
updatedDate=story.updatedDate |
|||
tags=story.tags |
|||
author=story.author /> |
|||
</content-list> |
@ -0,0 +1,11 @@ |
|||
<menu user=input.user> |
|||
<menu-header>Story</menu-header> |
|||
<menu-link selected=input.selected.index icon="S" href="/story/">Stories</menu-link> |
|||
<menu-gap /> |
|||
<menu-header>Categories</menu-header> |
|||
<menu-link for(category in (input.categories||[])) selected=(input.selected.category == category.name) href=`/story/by-category/${category.name}` icon=category.name.charAt(0)>${category.name}</menu-link> |
|||
<menu-gap /> |
|||
<menu-header>Tags</menu-header> |
|||
<menu-link for(tag in (input.menuTags||[])) selected=(input.selected.tag === `${tag.kind}:${tag.name}`) href=`/story/by-tag/${tag.kind}/${tag.name}` textClass=`color-tag-${tag.kind.toLowerCase()}` icon=tag.kind.charAt(0)>${tag.name}</menu-link> |
|||
<menu-link selected=input.selected.tags icon="T" href="/story/tag-list/">Tags</menu-link> |
|||
</menu> |
@ -0,0 +1,9 @@ |
|||
<include("../layout", {title: "Stories", site: "story"})> |
|||
<@body> |
|||
<background src="/assets/images/bg.png" opacity=0.25 /> |
|||
<story-menu categories=input.categories selected=(input.selected || {}) menuTags=input.menuTags user=input.user /> |
|||
<main> |
|||
<story-list stories=input.stories /> |
|||
</main> |
|||
</@body> |
|||
</include> |
@ -0,0 +1,8 @@ |
|||
<include("../layout", {title: "Stories", site: "story"})> |
|||
<@body> |
|||
<background src="/assets/images/bg.png" opacity=0.25 /> |
|||
<story-menu categories=input.categories selected=(input.selected || {}) menuTags=input.menuTags user=input.user /> |
|||
<main> |
|||
</main> |
|||
</@body> |
|||
</include> |
@ -0,0 +1,29 @@ |
|||
module.exports = (req, res, next) => { |
|||
if (res.marko) { |
|||
res.markoAsync = async(template, input) => { |
|||
const locals = Object.assign((res.locals || {}), input) |
|||
|
|||
for (const key in locals) { |
|||
const value = locals[key] |
|||
if (value instanceof Promise) { |
|||
locals[key] = await value |
|||
} |
|||
} |
|||
|
|||
return res.marko(template, locals) |
|||
} |
|||
} |
|||
|
|||
if (req.user) { |
|||
res.locals.user = { |
|||
loggedIn: true, |
|||
name: req.user._json.name, |
|||
} |
|||
} else { |
|||
res.locals.user = { |
|||
loggedIn: false, |
|||
} |
|||
} |
|||
|
|||
next() |
|||
} |
@ -0,0 +1,29 @@ |
|||
const Auth0Strategy = require("passport-auth0") |
|||
const passport = require("passport") |
|||
const config = require("../config") |
|||
|
|||
//passport-auth0
|
|||
const strategy = new Auth0Strategy({ |
|||
domain: config.auth0.domain, |
|||
clientID: config.auth0.clientId, |
|||
clientSecret: config.auth0.secret, // Replace this with the client secret for your app
|
|||
callbackURL: config.root + "/auth/callback" |
|||
}, (accessToken, refreshToken, extraParams, profile, done) => { |
|||
// accessToken is the token to call Auth0 API (not needed in the most cases)
|
|||
// extraParams.id_token has the JSON Web Token
|
|||
// profile has all the information from the user
|
|||
return done(null, profile); |
|||
} |
|||
) |
|||
|
|||
passport.use(strategy) |
|||
|
|||
passport.serializeUser(function(user, done) { |
|||
done(null, user); |
|||
}); |
|||
|
|||
passport.deserializeUser(function(user, done) { |
|||
done(null, user); |
|||
}); |
|||
|
|||
module.exports = passport |
@ -0,0 +1,15 @@ |
|||
const session = require("express-session") |
|||
const config = require("../config") |
|||
var RedisStore = require('connect-redis')(session); |
|||
|
|||
var sess = { |
|||
secret: config.session.secret, |
|||
store: new RedisStore(config.redis), |
|||
cookie: {}, |
|||
resave: false, |
|||
saveUninitialized: true, |
|||
} |
|||
|
|||
sess.cookie.secure = config.session.secure |
|||
|
|||
module.exports = session(sess) |
4922
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,41 @@ |
|||
{ |
|||
"name": "@gisle/rpdata-frontend", |
|||
"version": "0.1.0", |
|||
"description": "RPData (www.aiterp.net) frontend/proxy", |
|||
"main": "./index.js", |
|||
"scripts": { |
|||
"dev": "browser-refresh server.js", |
|||
"prod": "NODE_ENV=production node server.js" |
|||
}, |
|||
"author": "Gisle Aune <dev@gisle.me>", |
|||
"license": "MIT", |
|||
"homepage": "https://github.com/marko-js-samples/marko-express", |
|||
"dependencies": { |
|||
"@lasso/marko-taglib": "^1.0.12", |
|||
"babel-preset-es2015": "^6.24.1", |
|||
"body-parser": "^1.18.3", |
|||
"browser-refresh": "^1.7.3", |
|||
"browser-refresh-taglib": "^1.1.0", |
|||
"connect-ensure-login": "^0.1.1", |
|||
"connect-redis": "^3.3.3", |
|||
"empty-module": "0.0.2", |
|||
"es6-promise": "^4.2.4", |
|||
"express": "^4.16.3", |
|||
"express-http-proxy": "^1.2.0", |
|||
"express-session": "^1.15.6", |
|||
"graphql-query-compress": "^1.1.0", |
|||
"isomorphic-fetch": "^2.2.1", |
|||
"jsonwebtoken": "^8.3.0", |
|||
"lasso": "^3.2.6", |
|||
"lasso-babel-transform": "^1.0.2", |
|||
"lasso-less": "^3.0.1", |
|||
"lasso-marko": "^2.4.4", |
|||
"markdown-it": "^8.4.2", |
|||
"markdown-it-container": "^2.0.0", |
|||
"markdown-it-regexp": "^0.4.0", |
|||
"marko": "^4.13.4", |
|||
"moment": "^2.22.2", |
|||
"passport": "^0.4.0", |
|||
"passport-auth0": "^1.0.0" |
|||
} |
|||
} |
@ -0,0 +1,46 @@ |
|||
const express = require("express") |
|||
const passport = require("../middleware/passport") |
|||
|
|||
const router = express.Router() |
|||
|
|||
function saveReferer(req, res, next) { |
|||
req.session.loginReferrer = req.get("Referer") |
|||
req.session.save() |
|||
|
|||
next() |
|||
} |
|||
|
|||
// Perform the login, after login Auth0 will redirect to callback
|
|||
router.get("/login", saveReferer, passport.authenticate("auth0", {scope: "openid username profile"}), (req, res) => { |
|||
res.redirect("/") |
|||
}) |
|||
|
|||
router.get("/callback", passport.authenticate("auth0", { failureRedirect: "/auth/login" }), (req, res) => { |
|||
if (req.user == null) { |
|||
throw new Error("user null"); |
|||
} |
|||
|
|||
res.redirect(req.session.loginReferrer || "/"); |
|||
}) |
|||
|
|||
router.get("/user", (req, res) => { |
|||
if (req.user == null) { |
|||
return res.status(401).json({error: {message: "Unauthorized"}}) |
|||
} |
|||
|
|||
const name = req.user._json.name |
|||
|
|||
// TODO: Grab data from rpdata-api user database
|
|||
|
|||
res.json({data: { |
|||
name |
|||
}}) |
|||
}) |
|||
|
|||
router.get("/logout", saveReferer, (req, res) => { |
|||
req.logout() |
|||
|
|||
res.redirect(req.session.loginReferrer || "/"); |
|||
}) |
|||
|
|||
module.exports = router |
@ -0,0 +1,56 @@ |
|||
const express = require("express") |
|||
const jwt = require("jsonwebtoken") |
|||
|
|||
const config = require("../config") |
|||
const { query } = require("../rpdata/client") |
|||
|
|||
const router = express.Router() |
|||
|
|||
router.post("/", (req, res) => { |
|||
if (!req.header("Content-Type").startsWith("application/json")) { |
|||
res.status(400).json({errors: [{message: "Incorrect input type, expected application/json"}]}) |
|||
return |
|||
} |
|||
|
|||
const user = res.locals.user |
|||
const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null") |
|||
let authorization = req.header("Authorization") |
|||
if (authorization == "null") { |
|||
authorization = "" |
|||
} |
|||
|
|||
if (!authorization && permissions.length > 0 && user.loggedIn) { |
|||
authorization = `Bearer ${generateToken(user.name, permissions)}` |
|||
} |
|||
|
|||
fetch(config.graphqlEndpoint, { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"Authorization": authorization, |
|||
}, |
|||
body: JSON.stringify(req.body), |
|||
credentials: "include", |
|||
}).then(fetchRes => { |
|||
res.setHeader("Content-Type", fetchRes.headers.get("Content-Type")) |
|||
res.status(fetchRes.status) |
|||
|
|||
return fetchRes.json() |
|||
}).then(json => { |
|||
res.json(json) |
|||
}).catch(err => { |
|||
res.status(500).text(err) |
|||
return null |
|||
}) |
|||
}) |
|||
|
|||
|
|||
/** |
|||
* @param {string} user |
|||
* @param {string[]} permissions |
|||
*/ |
|||
function generateToken(user, permissions) { |
|||
return jwt.sign({user, permissions, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}}) |
|||
} |
|||
|
|||
module.exports = router |
@ -0,0 +1,19 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const {logHeaderApi} = require("../../rpdata/api/LogHeader") |
|||
|
|||
const listTemplate = require("../../marko/page/logs/list.marko") |
|||
|
|||
router.get("/", async(req, res) => { |
|||
try { |
|||
res.markoAsync(listTemplate, { |
|||
logs: logHeaderApi.list({limit: 0}), |
|||
selected: {index: true}, |
|||
}) |
|||
} catch(err) { |
|||
console.error(err) |
|||
} |
|||
}) |
|||
|
|||
module.exports = router |
@ -0,0 +1,13 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const {storyApi} = require("../../rpdata/api/Story") |
|||
|
|||
const viewTemplate = require("../../marko/page/story-content/view.marko") |
|||
|
|||
module.exports = async(req, res, next) => { |
|||
res.markoAsync(viewTemplate, { |
|||
story: storyApi.find(req.params.id), |
|||
selected: {index: true}, |
|||
}) |
|||
} |
@ -0,0 +1,22 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const common = require("./common") |
|||
const {storyApi} = require("../../rpdata/api/Story") |
|||
const {Tag} = require("../../rpdata/api/Tag") |
|||
|
|||
const listTemplate = require("../../marko/page/story/list.marko") |
|||
|
|||
router.get("/:category", common, async(req, res) => { |
|||
try { |
|||
res.markoAsync(listTemplate, { |
|||
stories: storyApi.list({category: req.params.category}), |
|||
selected: {category: req.params.category}, |
|||
}) |
|||
} catch(err) { |
|||
console.error(err) |
|||
} |
|||
}) |
|||
|
|||
module.exports = router |
|||
|
@ -0,0 +1,24 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const common = require("./common") |
|||
const {storyApi} = require("../../rpdata/api/Story") |
|||
const {Tag} = require("../../rpdata/api/Tag") |
|||
|
|||
const listTemplate = require("../../marko/page/story/list.marko") |
|||
|
|||
router.get("/:tagkind/:tagname", common, async(req, res) => { |
|||
try { |
|||
const tag = new Tag(req.params.tagkind, req.params.tagname) |
|||
|
|||
res.markoAsync(listTemplate, { |
|||
stories: storyApi.list({tags: tag}), |
|||
menuTags: [tag], |
|||
selected: {tag: `${tag.kind}:${tag.name}`}, |
|||
}) |
|||
} catch(err) { |
|||
console.error(err) |
|||
} |
|||
}) |
|||
|
|||
module.exports = router |
@ -0,0 +1,7 @@ |
|||
const {storyApi} = require("../../rpdata/api/Story") |
|||
|
|||
module.exports = (req, res, next) => { |
|||
res.locals.categories = storyApi.categories() |
|||
|
|||
next() |
|||
} |
@ -0,0 +1,20 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const common = require("./common") |
|||
const {storyApi} = require("../../rpdata/api/Story") |
|||
|
|||
const listTemplate = require("../../marko/page/story/list.marko") |
|||
|
|||
router.get("/", common, (req, res) => { |
|||
try { |
|||
res.markoAsync(listTemplate, { |
|||
stories: storyApi.list(), |
|||
selected: {index: true}, |
|||
}) |
|||
} catch(err) { |
|||
console.error(err) |
|||
} |
|||
}) |
|||
|
|||
module.exports = router |
@ -0,0 +1,20 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const common = require("./common") |
|||
const {tagApi} = require("../../rpdata/api/Tag") |
|||
|
|||
const tagListTemplate = require("../../marko/page/story/tag-list.marko") |
|||
|
|||
router.get("/", common, (req, res) => { |
|||
try { |
|||
res.markoAsync(tagListTemplate, { |
|||
tags: tagApi.list(), |
|||
selected: {tags: true}, |
|||
}) |
|||
} catch(err) { |
|||
console.error(err) |
|||
} |
|||
}) |
|||
|
|||
module.exports = router |
@ -0,0 +1,53 @@ |
|||
const {query} = require("../client") |
|||
|
|||
class Chapter { |
|||
/** |
|||
* @param {string} id |
|||
* @param {string} title |
|||
* @param {string} author |
|||
* @param {string} source |
|||
* @param {string | Date} createdDate |
|||
* @param {string | Date} fictionalDate |
|||
* @param {string | Date} editedDate |
|||
*/ |
|||
constructor(id, title, author, source, createdDate, fictionalDate, editedDate) { |
|||
this.id = id |
|||
this.title = title |
|||
this.author = author |
|||
this.source = source |
|||
this.createdDate = new Date(createdDate) |
|||
this.fictionalDate = new Date(fictionalDate) |
|||
this.editedDate = new Date(editedDate) |
|||
} |
|||
|
|||
/** |
|||
* Query the chapter's editable fields and update this object. This promise returns a boolean |
|||
* whether there has been a change. |
|||
* |
|||
* @returns {Promise<boolean>} `true` if the story was edited in the meantime |
|||
*/ |
|||
update() { |
|||
return query(`
|
|||
query UpdateChapter($id: String!) { |
|||
update: chapter(id: $id) { |
|||
title |
|||
source |
|||
fictionalDate |
|||
editedDate |
|||
} |
|||
} |
|||
`, {id: this.id}).then(({update}) => {
|
|||
const editedDate = new Date(udpate.editedDate) |
|||
const wasEdited = editedDate.getTime() != this.editedDate.getTime() |
|||
|
|||
this.title = update.title |
|||
this.source = update.source |
|||
this.fictionalDate = new Date(update.fictionalDate) |
|||
this.editedDate = editedDate |
|||
|
|||
return wasEdited |
|||
}) |
|||
} |
|||
} |
|||
|
|||
module.exports = {Chapter} |
@ -0,0 +1,5 @@ |
|||
const {query} = require("../client") |
|||
|
|||
class Character { |
|||
|
|||
} |
@ -0,0 +1,84 @@ |
|||
const {query} = require("../client") |
|||
|
|||
class LogHeader { |
|||
/** |
|||
* Construct a log header. You should probably use the logHeaderApi instead of doing |
|||
* this manually, even for mutations. |
|||
* |
|||
* @param {string} id |
|||
* @param {Date|string} date |
|||
* @param {string} channelName |
|||
* @param {string} title |
|||
* @param {string} description |
|||
* @param {string} event |
|||
* @param {boolean} open |
|||
* @param {{id:string,name:string,shortName:string,author:string}[]} characters |
|||
*/ |
|||
constructor(id, shortId, date, channelName, title, description, event, open, characters) { |
|||
this.id = id |
|||
this.shortId = shortId |
|||
this.date = new Date(date) |
|||
this.channelName = channelName |
|||
this.title = title || null |
|||
this.description = description || null |
|||
this.event = event || null |
|||
this.open = open |
|||
this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author)) |
|||
} |
|||
} |
|||
|
|||
class LogHeaderCharacter { |
|||
/** |
|||
* Construct a log header character list entry. |
|||
* |
|||
* @param {string} id |
|||
* @param {string} name |
|||
* @param {string} shortName |
|||
* @param {string} author |
|||
*/ |
|||
constructor(id, name, shortName, author) { |
|||
this.id = id |
|||
this.name = name |
|||
this.shortName = shortName |
|||
this.author = author |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* logHeaderApi contains the API queries for the LogHeader frontend model, which is a subset of the |
|||
* logs model. |
|||
*/ |
|||
const logHeaderApi = { |
|||
/** |
|||
* Call `stories(filter)` query |
|||
* |
|||
* @param {{search:string, channels:string|string[], events:string|string[], open:boolean, characters:string|string[], limit:number}} filter |
|||
* @returns {Promise<LogHeader[]>} |
|||
*/ |
|||
list(filter = {}) { |
|||
return query(`
|
|||
query LogHeaders($filter: LogsFilter) { |
|||
headers: logs(filter:$filter) { |
|||
id |
|||
shortId |
|||
date |
|||
channelName |
|||
title |
|||
description |
|||
event |
|||
open |
|||
characters { |
|||
id |
|||
name |
|||
shortName |
|||
author |
|||
} |
|||
} |
|||
} |
|||
`, {filter}).then(({headers}) => {
|
|||
return headers.map(h => new LogHeader(h.id, h.shortId, h.date, h.channelName, h.title, h.description, h.event, h.open, h.characters)) |
|||
}) |
|||
}, |
|||
} |
|||
|
|||
module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi} |
@ -0,0 +1,147 @@ |
|||
const {query} = require("../client") |
|||
const {Tag} = require("./Tag") |
|||
const {Chapter} = require("./Chapter") |
|||
|
|||
class Story { |
|||
/** |
|||
* Construct a story object. You should use the API instead of calling this. |
|||
* |
|||
* @param {string} id |
|||
* @param {string} name |
|||
* @param {string} author |
|||
* @param {"Info"|"News"|"Document"|"Background"|"Story"} category Story's category |
|||
* @param {string} createdDate |
|||
* @param {string} updatedDate |
|||
* @param {string} fictionalDate |
|||
* @param {{kind:string, name:string}[]} tags |
|||
* @param {Chapter[]} chapters |
|||
*/ |
|||
constructor(id, name, author, category, createdDate, updatedDate, fictionalDate, tags, chapters) { |
|||
this.id = id |
|||
this.name = name |
|||
this.author = author |
|||
this.category = category |
|||
this.createdDate = new Date(createdDate) |
|||
this.updatedDate = new Date(updatedDate) |
|||
this.fictionalDate = new Date(fictionalDate) |
|||
this.tags = tags.map(dt => new Tag(dt.kind, dt.name)) |
|||
this.chapters = chapters != null ? chapters.map(c => new Chapter(c.id, c.title, c.author, c.source, c.createdDate, c.fictionalDate, c.editedDate)) : null |
|||
} |
|||
} |
|||
|
|||
class StoryCategory { |
|||
constructor(name, description) { |
|||
this.name = name |
|||
this.description = description |
|||
} |
|||
} |
|||
|
|||
const data = { |
|||
categories: null |
|||
} |
|||
|
|||
/** |
|||
* storyApi contains the API queries |
|||
*/ |
|||
const storyApi = { |
|||
/** |
|||
* Call `stories(filter)` query |
|||
* |
|||
* @param {{author:string, category:string, tags:Tag[]|Tag, unlisted: boolean, open: boolean, earliestFictionalDate:Date|string, latestFictionalDate:Date|string, limit:number}} filter |
|||
* @returns {Promise<Story[]>} |
|||
*/ |
|||
list(filter = {}) { |
|||
if (filter.earliestFictionalDate != null && typeof(filter.earliestFictionalDate) !== "string") { |
|||
filter.earliestFictionalDate = filter.earliestFictionalDate.toISOString() |
|||
} |
|||
if (filter.latestFictionalDate != null && typeof(filter.latestFictionalDate) !== "string") { |
|||
filter.latestFictionalDate = filter.latestFictionalDate.toISOString() |
|||
} |
|||
|
|||
return query(`
|
|||
query ListStories($filter: StoriesFilter) { |
|||
stories(filter: $filter) { |
|||
id |
|||
name |
|||
author |
|||
category |
|||
tags { |
|||
kind |
|||
name |
|||
} |
|||
createdDate |
|||
fictionalDate |
|||
updatedDate |
|||
} |
|||
} |
|||
`, {filter}).then(({stories}) => {
|
|||
return stories.map(d => new Story(d.id, d.name, d.author, d.category, d.createdDate, d.updatedDate, d.fictionalDate, d.tags)) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* @param {string} id |
|||
* @returns {Promise<Story>} |
|||
*/ |
|||
find(id) { |
|||
return query(`
|
|||
query FindStory($id: String!) { |
|||
story(id: $id) { |
|||
id |
|||
name |
|||
author |
|||
category |
|||
tags { |
|||
kind |
|||
name |
|||
} |
|||
createdDate |
|||
fictionalDate |
|||
updatedDate |
|||
chapters { |
|||
id |
|||
title |
|||
author |
|||
source |
|||
createdDate |
|||
fictionalDate |
|||
editedDate |
|||
} |
|||
} |
|||
} |
|||
`, {id}).then(({story}) => {
|
|||
return new Story( |
|||
story.id, story.name, story.author, story.category, |
|||
story.createdDate, story.updatedDate, story.fictionalDate, |
|||
story.tags, story.chapters |
|||
) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Call `__type(name: "StoryCategory")` query and extracts the catogires from it |
|||
* |
|||
* @returns {Promise<StoryCategory[]>} |
|||
*/ |
|||
categories() { |
|||
if (data.categories !== null) { |
|||
return data.categories |
|||
} |
|||
|
|||
return query(`
|
|||
query ListStoryCategories { |
|||
categoryType: __type(name: "StoryCategory") { |
|||
enumValues { |
|||
name |
|||
description |
|||
} |
|||
} |
|||
} |
|||
`, {}).then(({categoryType}) => {
|
|||
data.categories = categoryType.enumValues.map(d => new StoryCategory(d.name, d.description)) |
|||
return data.categories |
|||
}) |
|||
}, |
|||
} |
|||
|
|||
module.exports = {storyApi, Story} |
@ -0,0 +1,34 @@ |
|||
const {query} = require("../client") |
|||
|
|||
class Tag { |
|||
/** |
|||
* @param {string} kind |
|||
* @param {string} name |
|||
*/ |
|||
constructor(kind, name) { |
|||
this.kind = kind |
|||
this.name = name |
|||
} |
|||
} |
|||
|
|||
const tagApi = { |
|||
/** |
|||
* Call `tags` query |
|||
* |
|||
* @returns {Promise<Tag[]>} |
|||
*/ |
|||
list() { |
|||
return query(`
|
|||
query Tags { |
|||
tags { |
|||
kind |
|||
name |
|||
} |
|||
} |
|||
`, {}).then(({tags}) => {
|
|||
return tags.map(d => new Tag(d)) |
|||
}) |
|||
}, |
|||
} |
|||
|
|||
module.exports = {Tag, tagApi} |
@ -0,0 +1,6 @@ |
|||
module.exports = { |
|||
...require("./LogHeader"), |
|||
...require("./Chapter"), |
|||
...require("./Story"), |
|||
...require("./Tag"), |
|||
} |
@ -0,0 +1,40 @@ |
|||
const fetch = require("isomorphic-fetch") |
|||
const config = require("../config") |
|||
const compressQuery = require("graphql-query-compress") |
|||
|
|||
/** |
|||
* Run a GraphQL query against the rpdata backend |
|||
* |
|||
* @param {string} query The query to run |
|||
* @param {{[x:string]: any}} variables |
|||
* @param {{operationName:string, token: string, permissions: string[]}} options |
|||
*/ |
|||
function query(query, variables = {}, options = {}) { |
|||
return fetch(config.graphqlEndpoint, { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"Authorization": options.token ? `Bearer ${options.token}` : null, |
|||
"X-Permissions": options.permissions ? options.permissions.join(",") : null, |
|||
}, |
|||
body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}), |
|||
credentials: "include", |
|||
}).then(res => { |
|||
if (!res.ok) { |
|||
return Promise.reject([{ |
|||
message: `HTTP: ${res.statusText}`, |
|||
location: null, |
|||
}]) |
|||
} |
|||
|
|||
return res.json() |
|||
}).then(json => { |
|||
if (json.errors != null && json.errors.length > 0) { |
|||
return Promise.reject(json.errors) |
|||
} |
|||
|
|||
return json.data |
|||
}) |
|||
} |
|||
|
|||
module.exports = { query } |
@ -0,0 +1,101 @@ |
|||
// Setup global environment
|
|||
require("marko/node-require").install() |
|||
require("marko/express") |
|||
require("es6-promise").polyfill() |
|||
|
|||
// Load server config
|
|||
const config = require("./config") |
|||
|
|||
// Express depedencies
|
|||
const express = require("express") |
|||
const proxy = require("express-http-proxy") |
|||
const lasso = require("lasso") |
|||
const lassoMiddleware = require("lasso/middleware") |
|||
const bodyParser = require("body-parser") |
|||
|
|||
// Load middleware
|
|||
const passport = require("./middleware/passport") |
|||
const session = require("./middleware/session") |
|||
|
|||
// Setup express
|
|||
const app = express() |
|||
|
|||
// Configure lasso
|
|||
const isProduction = process.env.NODE_ENV === "production" |
|||
lasso.configure({ |
|||
plugins: [ |
|||
"lasso-marko", |
|||
"lasso-less", |
|||
], |
|||
outputDir: "./.static", // Place all generated JS/CSS/etc. files into the "static" dir
|
|||
bundlingEnabled: isProduction, // Only enable bundling in production
|
|||
minify: isProduction, // Only minify JS and CSS code in production
|
|||
fingerprintsEnabled: isProduction, // Only add fingerprints to URLs in production
|
|||
|
|||
require: { |
|||
builtins: { |
|||
fs: require.resolve("empty-module"), |
|||
}, |
|||
|
|||
transforms: isProduction ? [ |
|||
{ |
|||
transform: "lasso-babel-transform", |
|||
config: { |
|||
extensions: [".js", ".es6"], // Enabled file extensions. Default: [".js", ".es6"]
|
|||
babelOptions: { |
|||
presets: [ "es2015" ] |
|||
} |
|||
} |
|||
} |
|||
] : null |
|||
} |
|||
}) |
|||
|
|||
// Apply middleware
|
|||
app.use(session) |
|||
app.use(passport.initialize()) |
|||
app.use(passport.session()) |
|||
app.use(require("./middleware/locals")) |
|||
app.use(bodyParser.json({limit: "1mb"})) |
|||
app.use(bodyParser.text({limit: "256kb"})) |
|||
|
|||
// Static assets
|
|||
app.use("/assets", express.static(__dirname + "/assets")) |
|||
app.use(lassoMiddleware.serveStatic()) |
|||
|
|||
// Authentication
|
|||
app.use("/auth", require("./routes/auth")) |
|||
|
|||
// API Proxy
|
|||
app.use("/graphql", require("./routes/graphql")) |
|||
|
|||
// Page routes
|
|||
app.use("/story/", require("./routes/story")) |
|||
app.use("/story/by-category/", require("./routes/story/by-category")) |
|||
app.use("/story/by-tag/", require("./routes/story/by-tag")) |
|||
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("/logs/", require("./routes/logs")) |
|||
|
|||
// Entry point
|
|||
app.get("/", function(req, res) { |
|||
res.redirect("/story/") |
|||
}) |
|||
|
|||
// Render common templates
|
|||
require("./marko/page/story/list.marko").render() |
|||
require("./marko/page/logs/list.marko").render() |
|||
require("./marko/page/story-content/view.marko").render({story: {chapters: []}}) |
|||
|
|||
// Start server
|
|||
app.listen(config.port, function() { |
|||
console.log("Server started: http://localhost:" + config.port + "/") |
|||
|
|||
if (process.send) { |
|||
setTimeout(() => process.send("online"), 500) |
|||
} |
|||
}) |
|||
|
|||
// Handle shutdown signals (Docker needs this to shutdown quickly)
|
|||
process.on('SIGINT', () => process.exit(0)) |
|||
process.on('SIGTERM', () => process.exit(0)) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue