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.
This commit is contained in:
knotteye 2019-12-03 19:51:14 -06:00
parent 31426a0c41
commit 25cf8a37a2
13 changed files with 378 additions and 121 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
node_modules
site/live
config/local.toml
config/jwt.pem
config/generated.toml
install/db_setup.sql
build/**

View File

@ -50,4 +50,9 @@ port = 8000
record = false
publicEndpoint = 'live'
privateEndpoint = 'stream'
ffmpeg = ''
ffmpeg = ''
[transcode]
adapative = false
variants = 3
format = 'dash'

49
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 };
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 };

View File

@ -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 };

View File

@ -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);
});
}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

10
templates/login.njk Normal file
View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}