From 25cf8a37a2118d4b8703ab309da76b9d9ef74afb Mon Sep 17 00:00:00 2001
From: knotteye <knotteye@airmail.cc>
Date: Tue, 3 Dec 2019 19:51:14 -0600
Subject: [PATCH] Big Commit! Seriously, this one is pretty massive. Satyr now
 has proper sessions in the browser (like a real website), and a lot of
 changes were made.

API Endpoints were changed from requiring a username and password to requiring a valid JsonWebToken, obtained from /api/login
Satyr will generate a PEM format key for JWT signing and verification on startup if it can't find one at config/jwt.pem
This file was added to .gitignore
Two new depencies: cookie-parser and jose, for reading and signing JWTs.

Refactored http.ts into mutiple functions, with a couple helper functions related to cookies and JWT decoding and verification. Socket.IO chat will also automatically log in users with a valid JWT.

Refactor api.ts to reflect new requirements from endpoints.

Minor bugfix in server.ts so we don't throw an uncaught exception when rejecting a stream with an invalid key.

Transcode options readded to default.toml. They do nothing and they are not sane defaults. Both of those things are in the todo list.
---
 .gitignore                 |   1 +
 config/default.toml        |   7 +-
 package-lock.json          |  49 +++++-
 package.json               |   2 +
 src/api.ts                 |  39 +++--
 src/http.ts                | 336 +++++++++++++++++++++++++++++--------
 src/server.ts              |   2 +-
 templates/base.njk         |   2 +-
 templates/changepwd.njk    |   7 +-
 templates/changesk.njk     |  11 --
 templates/login.njk        |  10 ++
 templates/profile.njk      |  13 +-
 templates/registration.njk |  20 ++-
 13 files changed, 378 insertions(+), 121 deletions(-)
 delete mode 100644 templates/changesk.njk
 create mode 100644 templates/login.njk

diff --git a/.gitignore b/.gitignore
index 0cdbe36..fad7bdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 node_modules
 site/live
 config/local.toml
+config/jwt.pem
 config/generated.toml
 install/db_setup.sql
 build/**
diff --git a/config/default.toml b/config/default.toml
index ea03684..3ff79ca 100644
--- a/config/default.toml
+++ b/config/default.toml
@@ -50,4 +50,9 @@ port = 8000
 record = false
 publicEndpoint = 'live'
 privateEndpoint = 'stream'
-ffmpeg = ''
\ No newline at end of file
+ffmpeg = ''
+
+[transcode]
+adapative = false
+variants = 3
+format = 'dash'
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 582080b..9c53b90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "satyr",
-  "version": "0.4.3",
+  "version": "0.4.4",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -121,6 +121,16 @@
       "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
       "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
     },
+    "asn1.js": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz",
+      "integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==",
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
     "assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -257,6 +267,11 @@
       "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
       "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
     },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+    },
     "body-parser": {
       "version": "1.19.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -507,6 +522,22 @@
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
       "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
     },
+    "cookie-parser": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz",
+      "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==",
+      "requires": {
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+          "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+        }
+      }
+    },
     "cookie-signature": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -1837,6 +1868,14 @@
       "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
       "optional": true
     },
+    "jose": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-1.15.1.tgz",
+      "integrity": "sha512-Gp+53zIEb68qTuyagcalDMVn1s0WrxiGBQJbEjShOdv3CYmbPIJEAN0Qtn4rCa7XgODoEa7HHuz8GoYgIpIzog==",
+      "requires": {
+        "asn1.js": "^5.2.0"
+      }
+    },
     "json5": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
@@ -1940,6 +1979,11 @@
         "mime-db": "1.40.0"
       }
     },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -2984,8 +3028,7 @@
     "typescript": {
       "version": "3.6.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
-      "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==",
-      "dev": true
+      "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw=="
     },
     "union-value": {
       "version": "1.0.1",
diff --git a/package.json b/package.json
index 221094e..7b023d9 100644
--- a/package.json
+++ b/package.json
@@ -17,9 +17,11 @@
 		"bcrypt": "^3.0.6",
 		"body-parser": "^1.19.0",
 		"config": "^3.2.2",
+		"cookie-parser": "^1.4.4",
 		"dirty": "^1.1.0",
 		"express": "^4.17.1",
 		"flags": "^0.1.3",
+		"jose": "^1.15.1",
 		"mysql": "^2.17.1",
 		"node-media-server": ">=2.1.3 <3.0.0",
 		"nunjucks": "^3.2.0",
diff --git a/src/api.ts b/src/api.ts
index 5c01cd6..7ff9944 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -1,4 +1,5 @@
 import * as db from "./database"
+import { unregisterUser } from "./irc";
 
 var config: any;
 function init(conf: object){
@@ -20,18 +21,26 @@ async function register(name: string, password: string, confirm: string) {
 	return {"error":""};
 }
 
-async function update(name: string, password: string, title: string, bio: string, record: boolean){
-	if(!name || !password) return {"error":"Insufficient parameters"};
-	let auth: boolean = await db.validatePassword(name, password);
-	if(!auth) return {"error":"Username or Password Incorrect"};
-	await db.query('UPDATE user_meta set title='+db.raw.escape(title)+', about='+db.raw.escape(bio)+' where username='+db.raw.escape(name));
-	if(!record) await db.query('UPDATE users set record_flag=false where username='+db.raw.escape(name));
-	else await db.query('UPDATE users set record_flag=true where username='+db.raw.escape(name));
+async function update(fields: object){
+	if(!fields['title'] && !fields['bio'] && (fields['rec'] !== 'true' && fields['rec'] !== 'false')) return {"error":"no valid fields specified"};
+	let qs: string = "";
+	let f: boolean = false;
+	if(fields['title']) {qs += ' user_meta.title='+db.raw.escape(fields['title']);f = true;}
+	if(fields['bio']) {
+		if(f) qs+=',';
+		qs += ' user_meta.about='+db.raw.escape(fields['bio']);
+		f=true;
+	}
+	if(typeof(fields['rec']) === 'boolean' || typeof(fields['rec']) === 'number') {
+		if(f) qs+=',';
+		qs += ' users.record_flag='+db.raw.escape(fields['rec']);
+	}
+	await db.query('UPDATE users,user_meta SET'+qs+' WHERE users.username='+db.raw.escape(fields['name'])+' AND user_meta.username='+db.raw.escape(fields['name']));
 	return {"success":""};
 }
 
 async function changepwd(name: string, password: string, newpwd: string){
-	if(!name || !password) return {"error":"Insufficient parameters"};
+	if(!name || !password || !newpwd) return {"error":"Insufficient parameters"};
 	let auth: boolean = await db.validatePassword(name, password);
 	if(!auth) return {"error":"Username or Password Incorrect"};
 	let newhash: string = await db.hash(newpwd);
@@ -39,13 +48,17 @@ async function changepwd(name: string, password: string, newpwd: string){
 	return {"success":""};
 }
 
-async function changesk(name: string, password: string){
-	if(!name || !password) return {"error":"Insufficient parameters"};
-	let auth: boolean = await db.validatePassword(name, password);
-	if(!auth) return {"error":"Username or Password Incorrect"};
+async function changesk(name: string){
 	let key: string = await db.genKey();
 	await db.query('UPDATE users set stream_key='+db.raw.escape(key)+'where username='+db.raw.escape(name)+' limit 1');
 	return {"success":key};
 }
 
-export { init, register, update, changepwd, changesk };
\ No newline at end of file
+async function login(name: string, password: string){
+	if(!name || !password) return {"error":"Insufficient parameters"};
+	let auth: boolean = await db.validatePassword(name, password);
+	if(!auth) return {"error":"Username or Password Incorrect"};
+	return false;
+}
+
+export { init, register, update, changepwd, changesk, login };
\ No newline at end of file
diff --git a/src/http.ts b/src/http.ts
index 2d2b5ff..94302c2 100644
--- a/src/http.ts
+++ b/src/http.ts
@@ -4,22 +4,35 @@ import * as bodyparser from "body-parser";
 import * as fs from "fs";
 import * as socketio from "socket.io";
 import * as http from "http";
+import * as cookies from "cookie-parser";
 import * as dirty from "dirty";
 import * as api from "./api";
 import * as db from "./database";
 import * as irc from "./irc";
+import { readFileSync, writeFileSync } from "fs";
+import { JWT, JWK } from "jose";
+import { strict } from "assert";
+import { parse } from "path";
 
 const app = express();
 const server = http.createServer(app);
 const io = socketio(server);
 const store = dirty();
+var jwkey;
+try{
+	jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
+} catch (e) {
+	console.log("No key found for JWT signing, generating one now.");
+	jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
+	writeFileSync('./config/jwt.pem', jwkey.toPEM(true));
+}
 var njkconf;
 
 async function init(satyr: any, port: number, ircconf: any){
 	njk.configure('templates', {
-		autoescape: true,
-		express   : app,
-		watch: false
+		autoescape	: true,
+		express   	: app,
+		watch		: true
 	});
 	njkconf ={
 		sitename: satyr.name,
@@ -28,88 +41,287 @@ async function init(satyr: any, port: number, ircconf: any){
 		rootredirect: satyr.rootredirect,
 		version: satyr.version
 	};
+	app.use(cookies());
 	app.use(bodyparser.json());
 	app.use(bodyparser.urlencoded({ extended: true }));
 	//site handlers
+	await initSite(satyr.registration);
+	//api handlers
+	await initAPI();
+	//static files if nothing else matches first
+	app.use(express.static(satyr.directory));
+	//404 Handler
+	app.use(function (req, res, next) {
+		if(tryDecode(req.cookies.Authorization)) {
+			res.status(404).render('404.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+		}
+		else res.status(404).render('404.njk', njkconf);
+		//res.status(404).render('404.njk', njkconf);
+	});
+	await initChat(ircconf);
+	server.listen(port);
+}
+
+async function newNick(socket, skip?: boolean) {
+	if(socket.handshake.headers['cookie'] && !skip){
+		let c = await parseCookie(socket.handshake.headers['cookie']);
+		let t = await validToken(c['Authorization']);
+		if(t) return t['username'];
+	}
+	//i just realized how shitty of an idea this is
+	let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000));
+	if(store.get(n)) return newNick(socket, true);
+	else {
+		store.set(n, socket.id);
+		return n;
+	}
+}
+
+async function chgNick(socket, nick) {
+	let rooms = Object.keys(socket.rooms);
+	for(let i=1;i<rooms.length;i++){
+		io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
+	}
+	store.rm(socket.nick);
+	store.set(nick, socket.id);
+	socket.nick = nick;
+}
+
+async function genToken(u: string){
+	return await JWT.sign({
+		username: u
+	}, jwkey, {
+		expiresIn: '1 week',
+		iat: true,
+		kid: false
+	});//set jwt
+}
+
+async function validToken(t: any){
+	try {
+		let token = JWT.verify(t, jwkey);
+		return token;
+	}
+	catch (err){
+		return false;
+	}
+}
+
+function tryDecode(t: any){
+	try {
+		return JWT.decode(t);
+	}
+	catch (err){
+		return false;
+	}
+}
+
+async function parseCookie(c){
+	if(typeof(c) !== 'string' || !c.includes('=')) return {};
+	return Object.assign({[c.split('=')[0].trim()]:c.split('=')[1].split(';')[0].trim()}, await parseCookie(c.split(/;(.+)/)[1]));
+}
+
+async function initAPI() {
+	app.get('/api/users/live', (req, res) => {
+		db.query('select username,title from user_meta where live=1 limit 10;').then((result) => {
+			res.send(result);
+		});
+	});
+	app.get('/api/users/live/:num', (req, res) => {
+		if(req.params.num > 50) req.params.num = 50;
+		db.query('select username,title from user_meta where live=1 limit '+req.params.num+';').then((result) => {
+			res.send(result);
+		});
+	});
+	app.post('/api/register', (req, res) => {
+		api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
+			if(result[0]) return genToken(req.body.username).then((t) => {
+				res.cookie('Authorization', t);
+				res.send(result);
+				return;
+			});
+			res.send(result);
+		});
+	});
+	app.post('/api/user/update', (req, res) => {
+		validToken(req.cookies.Authorization).then((t) => {
+			if(t) {
+				return api.update({name: t['username'],
+					title: "title" in req.body ? req.body.title : false,
+					bio: "bio" in req.body ? req.body.bio : false,
+					rec: "record" in req.body ? req.body.record : "NA"
+				}).then((r) => {
+					res.send(r);
+					return;
+				});
+			}
+			else {
+				res.send('{"error":"invalid token"}');
+				return;
+			}
+		});
+		/*api.update(req.body.username, req.body.password, req.body.title, req.body.bio, req.body.record).then((result) => {
+			res.send(result);
+		});*/
+	});
+	app.post('/api/user/password', (req, res) => {
+		validToken(req.cookies.Authorization).then((t) => {
+			if(t) {
+				return api.changepwd(t['username'], req.body.password, req.body.newpassword).then((r) => {
+					res.send(r);
+					return;
+				});
+			}
+			else {
+				res.send('{"error":"invalid token"}');
+				return;
+			}
+		});
+	});
+	app.post('/api/user/streamkey', (req, res) => {
+		validToken(req.cookies.Authorization).then((t) => {
+			if(t) {
+				api.changesk(t['username']).then((r) => {
+					res.send(r);
+				});
+			}
+			else {
+				res.send('{"error":"invalid token"}');
+			}
+		});
+	});
+	app.post('/api/login', (req, res) => {
+		if(req.cookies.Authorization) validToken(req.cookies.Authorization).then((t) => {
+			if(t) {
+				if(t['exp'] - 86400 < Math.floor(Date.now() / 1000)){
+					return genToken(t['username']).then((t) => {
+						res.cookie('Authorization', t);
+						res.send('{"success":""}');
+						return;
+					});
+				}
+				else {
+					res.send('{"success":"already verified"}');
+					return;
+				}
+			}
+			else {
+				res.send('{"error":"invalid token"}');
+				return;
+			}
+		});
+		else {
+			api.login(req.body.username, req.body.password).then((result) => {
+				if(!result){
+					genToken(req.body.username).then((t) => {
+						res.cookie('Authorization', t);
+						res.send('{"success":""}');
+					})
+				}
+				else {
+					res.send(result);
+				}
+			});
+		}
+	})
+}
+
+async function initSite(openReg) {
 	app.get('/', (req, res) => {
 		res.redirect(njkconf.rootredirect);
 	});
 	app.get('/about', (req, res) => {
-		res.render('about.njk', njkconf);
+		if(tryDecode(req.cookies.Authorization)) {
+			res.render('about.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+		}
+		else res.render('about.njk',njkconf);
 	});
 	app.get('/users', (req, res) => {
 		db.query('select username from users').then((result) => {
-			res.render('list.njk', Object.assign({list: result}, njkconf));
+			if(tryDecode(req.cookies.Authorization)) {
+				res.render('list.njk', Object.assign({list: result}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+			}
+			else res.render('list.njk', Object.assign({list: result}, njkconf));
+			//res.render('list.njk', Object.assign({list: result}, njkconf));
 		});
 	});
 	app.get('/users/live', (req, res) => {
 		db.query('select username,title from user_meta where live=1;').then((result) => {
-			res.render('live.njk', Object.assign({list: result}, njkconf));
-		});
-	});
-	app.get('/users/*', (req, res) => {
-		db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => {
-			if(result[0]){
-				res.render('user.njk', Object.assign(result[0], njkconf));
+			if(tryDecode(req.cookies.Authorization)) {
+				res.render('live.njk', Object.assign({list: result}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
 			}
-			else res.render('404.njk', njkconf);
+			else res.render('live.njk', Object.assign({list: result}, njkconf));
+			//res.render('live.njk', Object.assign({list: result}, njkconf));
 		});
 	});
-	app.get('/vods/*', (req, res) => {
-		db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => {
+	app.get('/users/:user', (req, res) => {
+		db.query('select username,title,about from user_meta where username='+db.raw.escape(req.params.user)).then((result) => {
 			if(result[0]){
-				fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => {
-					res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf));
+				if(tryDecode(req.cookies.Authorization)) {
+					res.render('user.njk', Object.assign(result[0], {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+				}
+				else res.render('user.njk', Object.assign(result[0], njkconf));
+				//res.render('user.njk', Object.assign(result[0], njkconf));
+			}
+			else if(tryDecode(req.cookies.Authorization)) {
+				res.status(404).render('404.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+			}
+			else res.status(404).render('404.njk', njkconf);
+		});
+	});
+	app.get('/vods/:user', (req, res) => {
+		db.query('select username from user_meta where username='+db.raw.escape(req.params.user)).then((result) => {
+			if(result[0]){
+				fs.readdir('./site/live/'+result[0].username, {withFileTypes: true} , (err, files) => {
+					if(tryDecode(req.cookies.Authorization)) {
+						res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+					}
+					else res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf));
+					//res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf));
 				});
 			}
-			else res.render('404.njk', njkconf);
+			else if(tryDecode(req.cookies.Authorization)) {
+				res.status(404).render('404.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+			}
+			else res.status(404).render('404.njk', njkconf);
 		});
 	});
+	app.get('/login', (req, res) => {
+		if(tryDecode(req.cookies.Authorization)) {
+			res.redirect(njkconf.rootredirect);
+		}
+		else res.render('login.njk',njkconf);
+	});
 	app.get('/register', (req, res) => {
-		res.render('registration.njk', njkconf);
+		if(tryDecode(req.cookies.Authorization) || !openReg) {
+			res.redirect(njkconf.rootredirect);
+		}
+		else res.render('registration.njk',njkconf);
 	});
 	app.get('/profile', (req, res) => {
-		res.render('profile.njk', njkconf);
+		if(tryDecode(req.cookies.Authorization)) {
+			res.render('profile.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+		}
+		else res.redirect(njkconf.rootredirect);
 	});
 	app.get('/changepwd', (req, res) => {
-		res.render('changepwd.njk', njkconf);
-	});
-	app.get('/changesk', (req, res) => {
-		res.render('changesk.njk', njkconf);
+		if(tryDecode(req.cookies.Authorization)) {
+			res.render('changepwd.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+		}
+		else res.redirect(njkconf.rootredirect);
 	});
 	app.get('/chat', (req, res) => {
 		res.render('chat.html', njkconf);
 	});
 	app.get('/help', (req, res) => {
-		res.render('help.njk', njkconf);
-	});
-	//api handlers
-	app.post('/api/register', (req, res) => {
-		api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
-			res.send(result);
-		});
-	});
-	app.post('/api/user', (req, res) => {
-		api.update(req.body.username, req.body.password, req.body.title, req.body.bio, req.body.record).then((result) => {
-			res.send(result);
-		});
-	});
-	app.post('/api/user/password', (req, res) => {
-		api.changepwd(req.body.username, req.body.password, req.body.newpassword).then((result) => {
-			res.send(result);
-		});
-	});
-	app.post('/api/user/streamkey', (req, res) => {
-		api.changesk(req.body.username, req.body.password).then((result) => {
-			res.send(result);
-		})
-	});
-	//static files if nothing else matches first
-	app.use(express.static('site'));
-	//404 Handler
-	app.use(function (req, res, next) {
-		res.status(404).render('404.njk', njkconf);
+		if(tryDecode(req.cookies.Authorization)) {
+			res.render('help.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
+		}
+		else res.render('help.njk',njkconf);
 	});
+}
+
+async function initChat(ircconf: any) {
 	//irc peering
 	if(ircconf.enable){
 		await irc.connect({
@@ -190,26 +402,6 @@ async function init(satyr: any, port: number, ircconf: any){
 			else socket.emit('ALERT', 'Not authorized to do that.');
 		});
 	});
-	server.listen(port);
-}
-
-async function newNick(socket) {
-	//i just realized how shitty of an idea this is
-	let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000));
-	if(store.get(n)) return newNick(socket);
-	else {
-		store.set(n, socket.id);
-		return n;
-	}
-}
-async function chgNick(socket, nick) {
-	let rooms = Object.keys(socket.rooms);
-	for(let i=1;i<rooms.length;i++){
-		io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
-	}
-	store.rm(socket.nick);
-	store.set(nick, socket.id);
-	socket.nick = nick;
 }
 
 export { init };
\ No newline at end of file
diff --git a/src/server.ts b/src/server.ts
index a52d807..62d8a8d 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -77,7 +77,7 @@ function init (mediaconfig: any, satyrconfig: any) {
 		if(app === satyrconfig.privateEndpoint) {
 			db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key));
 			db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
-				keystore.rm(results[0].username);
+				if(results[0]) keystore.rm(results[0].username);
 			});
 
 		}
diff --git a/templates/base.njk b/templates/base.njk
index a7aa9e7..7f2a642 100644
--- a/templates/base.njk
+++ b/templates/base.njk
@@ -8,7 +8,7 @@
 <body>
 	<div id="wrapper">
 		<div id="header">
-			<span style="float:left;"><h4><a href="/">{{ sitename }}</a> | <a href="/users">Users</a> <a href="/users/live">Live</a> <a href="/about">About</a></h4></span><span style="float:right;"><h4><a href="/help">Help</a> | <a href="/profile">Profile</a></h4></span>
+			<span style="float:left;"><h4><a href="/">{{ sitename }}</a> | <a href="/users">Users</a> <a href="/users/live">Live</a> <a href="/about">About</a></h4></span><span style="float:right;"><h4><a href="/help">Help</a> | {% if auth.is %}<a href="/profile">{{ auth.name }}{% else %}<a href="/login">Log In{% endif %}</a></h4></span>
 		</div>
 		<div id="content">
 			{% block content %}
diff --git a/templates/changepwd.njk b/templates/changepwd.njk
index aa29838..c3eaf03 100644
--- a/templates/changepwd.njk
+++ b/templates/changepwd.njk
@@ -1,11 +1,10 @@
 {% extends "base.njk" %}
 {% block content %}
-<h3>Change your password on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Update your <a href="/profile">profile</a> or <a href="/changesk">stream key</a>.</span>
+<h3>Change your password on {{ sitename }}</h3>
 <p></p>
 	<form action="/api/user/password" method="POST" target="responseFrame">
-		Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
-   		Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
-		New Password: </br><input type="password" name="newpassword" style="min-width: 300px"/></br>
+   		Old Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
+		New Password: </br><input type="password" name="newpassword" style="min-width: 300px"/></br></br>
 		<input type="submit" value="Submit">
 	</form>
 	<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
diff --git a/templates/changesk.njk b/templates/changesk.njk
deleted file mode 100644
index 30802a6..0000000
--- a/templates/changesk.njk
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "base.njk" %}
-{% block content %}
-<h3>Get a new stream key on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Update your <a href="/profile">profile</a> or <a href="/changepwd">password</a>.</span>
-<p></p>
-	<form action="/api/user/streamkey" method="POST" target="responseFrame">
-		Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
-   		Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
-		<input type="submit" value="Submit">
-	</form>
-	<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
-{% endblock %}
diff --git a/templates/login.njk b/templates/login.njk
new file mode 100644
index 0000000..f5cb4f8
--- /dev/null
+++ b/templates/login.njk
@@ -0,0 +1,10 @@
+{% extends "base.njk" %}
+{% block content %}
+<h3>Log in to {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br></br></span>
+			<form action="/api/login" method="POST" target="responseFrame">
+				Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
+        		Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
+				<input type="submit" value="Submit">
+			</form>
+			<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
+{% endblock %}
\ No newline at end of file
diff --git a/templates/profile.njk b/templates/profile.njk
index 465f061..a370a6c 100644
--- a/templates/profile.njk
+++ b/templates/profile.njk
@@ -1,14 +1,15 @@
 {% extends "base.njk" %}
 {% block content %}
-<h3>Update your profile on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Change your <a href="/changepwd">password</a> or <a href="/changesk">stream key</a>.</span>
+<h3>Update your profile on {{ sitename }}</h3><span style="font-size: small;">Or, change your <a href="/changepwd">password</a>.</span>
 <p></p>
-	<form action="/api/user" method="POST" target="responseFrame">
-		Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
-   		Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
+	<form action="/api/user/update" method="POST" target="responseFrame">
 		Stream Title: </br><input type="text" name="title" style="min-width: 300px"/></br>
 		Bio: </br><input type="text" name="bio" style="min-width: 300px; min-height: 150px;"/></br>
-		Record VODs: <input type="checkbox" name="record" value="true"></br>
-		<input type="submit" value="Submit">
+		Record VODs: <input type="radio" name="record" value="true"> Yes<input type="radio" name="record" value="false" /> No</br></br>
+		<input type="submit" value="Update Profile">
+	</form></br>
+	<form action="/api/user/streamkey" method="POST" target="responseFrame">
+		<input type="submit" value="Request New Stream Key">
 	</form>
 	<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
 {% endblock %}
\ No newline at end of file
diff --git a/templates/registration.njk b/templates/registration.njk
index 2bdfe0c..5b0ba91 100644
--- a/templates/registration.njk
+++ b/templates/registration.njk
@@ -1,17 +1,19 @@
 {% extends "base.njk" %}
 {% block content %}
-	<div id="jscontainer" style="height: 100%;">
-		<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">
+<h3>Register on {{ sitename }}</h3><span style="font-size: small;">Already registered? Log in <a href="/login">here</a>.</br></br></span>
+	<!--<div id="jscontainer" style="height: 100%;">
+		<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
 			<form action="/api/register" method="POST" target="responseFrame">
 				Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
         		Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
-				Confirm: </br><input type="password" name="confirm" style="min-width: 300px"/></br>
+				Confirm: </br><input type="password" name="confirm" style="min-width: 300px"/></br></br>
 				<input type="submit" value="Submit">
-			</form>
+			</form></br>
+			
+		<!--</div>
+		<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
+			{% include "tos.html" %}</br>
 			<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
-		</div>
-		<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">
-			{% include "tos.html" %}
-		</div>
-	</div>
+		<!--</div>
+	</div>-->
 {% endblock %}
\ No newline at end of file