Big commit. Implement handlers for everything that's currently rendered server side in the client-side frontend.

Add compiled templates file to .gitignore, will work out a system for making sure templates are compiled later.
Fix a couple bugs in the API and templates.

TODO for client-side rendering:
Make sure templates get compiled before running the server.
Add a config option to switch between server-side and client-side rendering
Fancy SPA stuff like intercepting links to render changes without a page-reload
merge-requests/27/head
knotteye 2020-10-14 07:44:19 -05:00
parent 57d0b0f856
commit 988e3473a7
8 changed files with 177 additions and 31 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ config/**/*
!config/.gitkeep !config/.gitkeep
install/db_setup.sql install/db_setup.sql
build/** build/**
site/templates.js

7
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "satyr", "name": "satyr",
"version": "0.9.3", "version": "0.9.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -963,6 +963,11 @@
"asn1.js": "^5.2.0" "asn1.js": "^5.2.0"
} }
}, },
"jwt-decode": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.0.0.tgz",
"integrity": "sha512-RBQv2MTm3FNKQkdzhEyQwh5MbdNgMa+FyIJIK5RMWEn6hRgRHr7j55cRxGhRe6vGJDElyi6f6u/yfkP7AoXddA=="
},
"lodash": { "lodash": {
"version": "4.17.20", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",

View File

@ -10,7 +10,7 @@
"setup": "sh install/setup.sh", "setup": "sh install/setup.sh",
"migrate": "ts-node src/migrate.ts", "migrate": "ts-node src/migrate.ts",
"invite": "ts-node src/cli.ts --invite", "invite": "ts-node src/cli.ts --invite",
"make-templates": "cp node_modules/nunjucks/browser/nunjucks-slim.js site &&nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js" "make-templates": "nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,37 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<link rel="stylesheet" type="text/css" href="/styles.css">
<link rel="stylesheet" type="text/css" href="/local.css">
<link rel="icon" type="image/svg" href="/logo.svg">
<script src="/nunjucks-slim.js"></script> <script src="/nunjucks-slim.js"></script>
<script src="/templates.js"></script> <script src="/templates.js"></script>
<script> <script>
nunjucks.configure({ autoescape: true }); nunjucks.configure({ autoescape: true });
</script> </script>
</head>
<body onload="render()">
<script> <script>
function render(){ //should check for and refresh login tokens on pageload..
switch(window.location.pathname){ if(document.cookie.match(/^(.*;)?\s*X-Auth-As\s*=\s*[^;]+(.*)?$/) !== null) {
case "/about.html": var xhr = new XMLHttpRequest();
document.body.innerHTML = nunjucks.render('about.html'); xhr.open("POST", "/api/login", true);
break; xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
case "/tos.html": xhr.send("");
document.body.innerHTML = nunjucks.render('tos.html');
break;
default:
document.body.innerHTML = nunjucks.render('404.njk');
}
}
function getContext(){
var conf = {
sitename: "",
domain: "",
email: "",
version: "",
registration: false
}
var info = new XMLHttpRequest();
info.onload = () => {
if(xhr.status === 200)
}
} }
</script> </script>
</head>
<body onload="render()">
<script src="/index.js"></script>
</body> </body>

141
site/index.js Normal file
View File

@ -0,0 +1,141 @@
async function render(){
var context = await getContext();
switch(window.location.pathname){
//nothing but context
case (window.location.pathname.match(/^\/about\/?$/) || {}).input:
document.body.innerHTML = nunjucks.render('about.njk', context);
break;
case (window.location.pathname.match(/^\/login\/?$/) || {}).input:
document.body.innerHTML = nunjucks.render('login.njk', context);
break;
case (window.location.pathname.match(/^\/register\/?$/) || {}).input:
if(!context.registration) window.location = '/';
document.body.innerHTML = nunjucks.render('registration.njk', context);
break;
case (window.location.pathname.match(/^\/changepwd\/?$/) || {}).input:
document.body.innerHTML = nunjucks.render('changepwd.njk', context);
break;
case (window.location.pathname.match(/^\/chat\/?$/) || {}).input:
document.body.innerHTML = nunjucks.render('chat.html', context);
break;
case (window.location.pathname.match(/^\/help\/?$/) || {}).input:
document.body.innerHTML = nunjucks.render('help.njk', context);
break;
//need to hit the API
case (window.location.pathname.match(/^\/users\/live\/?$/) || {}).input:
var list = JSON.parse(await makeRequest("POST", "/api/users/live", JSON.stringify({num: 50})));
document.body.innerHTML = nunjucks.render('live.njk', Object.assign({list: list.users}, context));
break;
case (window.location.pathname.match(/^\/users\/?$/) || {}).input:
var list = JSON.parse(await makeRequest("POST", "/api/users/all", JSON.stringify({num: 50})));
document.body.innerHTML = nunjucks.render('list.njk', Object.assign({list: list.users}, context));
break;
case (window.location.pathname.match(/^\/profile\/chat\/?$/) || {}).input:
if(!context.auth.name) window.location = '/login';
var config = JSON.parse(await makeRequest("GET", '/api/'+context.auth.name+'/config'));
config = {
integ: {
twitch: config.twitch,
xmpp: config.xmpp,
irc: config.irc,
discord: config.discord
}
};
document.body.innerHTML = nunjucks.render('chat_integ.njk', Object.assign(config, context));
break;
case (window.location.pathname.match(/^\/profile\/?$/) || {}).input:
if(!context.auth.name) window.location = '/login';
var config = JSON.parse(await makeRequest("GET", '/api/'+context.auth.name+'/config'));
config = {
meta: {
title: config.title,
about: config.about
},
rflag: {record_flag: config.record_flag},
twitch: config.twitch_mirror
};
document.body.innerHTML = nunjucks.render('profile.njk', Object.assign(config, context));
break;
//parsing slugs
case (window.location.pathname.match(/^\/invite\//) || {}).input: // /invite/:code
document.body.innerHTML = nunjucks.render('invite.njk', Object.assign({icode: window.location.pathname.substring(8)}, context));
break;
//slugs and API
case (window.location.pathname.match(/^\/users\/.+\/?$/) || {}).input: // /users/:user
if(window.location.pathname.substring(window.location.pathname.length - 1).indexOf('/') !== -1)
var usr = window.location.pathname.substring(7, window.location.pathname.length - 1);
else var usr = window.location.pathname.substring(7);
var config = JSON.parse(await makeRequest("GET", '/api/'+usr+'/config'));
if(!config.title){document.body.innerHTML = nunjucks.render('404.njk', context); break;}
document.body.innerHTML = nunjucks.render('user.njk', Object.assign({about: config.about, title: config.title, username: config.username}, context));
break;
case (window.location.pathname.match(/^\/vods\/.+\/manage\/?$/) || {}).input: // /vods/:user/manage
var usr = window.location.pathname.substring(6, (window.location.pathname.length - 7));
if(context.auth.name !== usr) window.location = '/vods/'+usr;
var vods = JSON.parse(await makeRequest("GET", '/api/'+usr+'/vods'));
document.body.innerHTML = nunjucks.render('managevods.njk', Object.assign({user: usr, list: vods.vods.filter(fn => fn.name.endsWith('.mp4'))}, context));
break;
case (window.location.pathname.match(/^\/vods\/.+\/?$/) || {}).input: // /vods/:user
if(window.location.pathname.substring(window.location.pathname.length - 1).indexOf('/') !== -1)
var usr = window.location.pathname.substring(6, window.location.pathname.length - 1);
else var usr = window.location.pathname.substring(6);
var vods = JSON.parse(await makeRequest("GET", '/api/'+usr+'/vods'));
document.body.innerHTML = nunjucks.render('vods.njk', Object.assign({user: usr, list: vods.vods.filter(fn => fn.name.endsWith('.mp4'))}, context));
break;
//root
case "/":
window.location = '/users/live';
break;
case "":
window.location = '/users/live';
break;
//404
default:
document.body.innerHTML = nunjucks.render('404.njk', context);
}
}
async function getContext(){
var info = JSON.parse(await makeRequest('GET', '/api/instance/info'));
info.sitename = info.name;
info.name = null;
info.auth = {
is: document.cookie.match(/^(.*;)?\s*X-Auth-As\s*=\s*[^;]+(.*)?$/) !== null,
name: parseCookie(document.cookie)['X-Auth-As']
}
return info;
}
function makeRequest(method, url, payload) {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
resolve(xhr.response);
} else {
reject({
status: this.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
!payload ? xhr.send() : xhr.send(payload);
});
}
function parseCookie(c){
if(typeof(c) !== 'string' || !c.includes('=')) return {};
return Object.assign({[c.split('=')[0].trim()]:c.split('=')[1].split(';')[0].trim()}, parseCookie(c.split(/;(.+)/)[1]));
}
function handleLoad() {
var r = JSON.parse(document.getElementById('responseFrame').contentDocument.documentElement.textContent).success
if (typeof(r) !== 'undefined') window.location.href = '/profile'
}

View File

@ -87,9 +87,11 @@ async function getConfig(username: string, all?: boolean): Promise<object>{
let users = await db.query('SELECT stream_key,record_flag FROM users WHERE username='+db.raw.escape(username)); let users = await db.query('SELECT stream_key,record_flag FROM users WHERE username='+db.raw.escape(username));
if(users[0]) Object.assign(t, users[0]); if(users[0]) Object.assign(t, users[0]);
let usermeta = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username)); let usermeta = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username));
if(usermeta[0]) Object.assign(t, users[0]); if(usermeta[0]) Object.assign(t, usermeta[0]);
let ci = await db.query('SELECT irc,xmpp,twitch,discord FROM chat_integration WHERE username='+db.raw.escape(username)); let ci = await db.query('SELECT irc,xmpp,twitch,discord FROM chat_integration WHERE username='+db.raw.escape(username));
if(ci[0]) Object.assign(t, ci[0]); if(ci[0]) Object.assign(t, ci[0]);
let tw = await db.query('SELECT enabled,twitch_key FROM twitch_mirror WHERE username='+db.raw.escape(username));
if(tw[0]) t['twitch_mirror'] = Object.assign({}, tw[0]);
} }
else { else {
let um = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username)); let um = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username));

View File

@ -75,6 +75,15 @@ async function init(){
} }
async function initFE(){ async function initFE(){
app.get('/', (req, res) => {
res.redirect(config['satyr']['rootredirect']);
});
app.get('/nunjucks-slim.js', (req, res) => {
res.sendFile(process.cwd()+'/node_modules/nunjucks/browser/nunjucks-slim.js');
});
app.get('/chat', (req, res) => {
res.sendFile(process.cwd()+'/templates/chat.html');
});
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(process.cwd()+'/'+config['http']['directory']+'/index.html'); res.sendFile(process.cwd()+'/'+config['http']['directory']+'/index.html');
}); });
@ -368,6 +377,7 @@ async function initAPI() {
if(req.cookies.Authorization) validToken(req.cookies.Authorization).then((t) => { if(req.cookies.Authorization) validToken(req.cookies.Authorization).then((t) => {
if(t) { if(t) {
if(t['exp'] - 86400 < Math.floor(Date.now() / 1000)){ if(t['exp'] - 86400 < Math.floor(Date.now() / 1000)){
res.cookie('X-Auth-As', t['username'], {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'});
return genToken(t['username']).then((t) => { return genToken(t['username']).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'}); res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.json({success:""}); res.json({success:""});
@ -389,6 +399,7 @@ async function initAPI() {
if(!result){ if(!result){
genToken(req.body.username).then((t) => { genToken(req.body.username).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'}); res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.cookie('X-Auth-As', req.body.username, {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'});
res.json({success:""}); res.json({success:""});
}) })
} }

View File

@ -8,7 +8,7 @@
{% else %} {% else %}
No recordings found! No recordings found!
{% endeach %} {% endeach %}
<input type="submit" value="Delete"> </br><input type="submit" value="Delete">
</form> </form>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe> <iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
{% endblock %} {% endblock %}