Merge branch 'invite-codes' into 'develop'

Invite codes

See merge request knotteye/satyr!26
merge-requests/26/merge
knotteye 2020-10-13 21:31:24 +00:00
commit 99879fd91e
8 changed files with 92 additions and 9 deletions

View File

@ -91,7 +91,9 @@ Register a new user.
**Authentication**: no **Authentication**: no
**Parameters**: Username, password, confirm **Parameters**: Username, password, confirm, invite(optional)
Invite is an optional invite code to bypass disabled registration.
**Response**: If successful, returns a json object with the users stream key. Otherwise returns `{error: "error reason"}` **Response**: If successful, returns a json object with the users stream key. Otherwise returns `{error: "error reason"}`

View File

@ -1,14 +1,15 @@
{ {
"name": "satyr", "name": "satyr",
"version": "0.9.3", "version": "0.9.4",
"description": "A livestreaming server.", "description": "A livestreaming server.",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"author": "knotteye", "author": "knotteye",
"scripts": { "scripts": {
"start": "ts-node src/index.ts", "start": "ts-node src/index.ts",
"user": "ts-node src/cli.ts", "cli": "ts-node src/cli.ts",
"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"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,9 +1,10 @@
import * as db from "./database"; import * as db from "./database";
import * as base64id from "base64id";
import { config } from "./config"; import { config } from "./config";
import {unlink} from "fs"; import {unlink} from "fs";
async function register(name: string, password: string, confirm: string): Promise<object> { async function register(name: string, password: string, confirm: string, invite?: boolean): Promise<object> {
if(!config['satyr']['registration']) return {"error":"registration disabled"}; if(!config['satyr']['registration'] && !invite) return {"error":"registration disabled"};
if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"}; if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
if(password !== confirm) return {"error":"mismatched passwords"}; if(password !== confirm) return {"error":"mismatched passwords"};
for(let i=0;i<config['satyr']['restrictedNames'].length;i++){ for(let i=0;i<config['satyr']['restrictedNames'].length;i++){
@ -97,4 +98,21 @@ async function getConfig(username: string, all?: boolean): Promise<object>{
return t; return t;
} }
export { register, update, changepwd, changesk, login, updateChat, deleteVODs, getConfig }; async function genInvite(): Promise<string>{
var invitecode: string = base64id.generateId();
await db.query('INSERT INTO invites (code) VALUES (\"'+invitecode+'\")');
return invitecode;
}
async function validInvite(code: string): Promise<boolean>{
if(typeof(code) !== "string" || code === "") return false;
var result = await db.query('SELECT code FROM invites WHERE code='+db.raw.escape(code));
if(!result[0] || result[0]['code'] !== code) return false;
return true;
}
async function useInvite(code: string): Promise<void>{
if(validInvite(code)) await db.query('DELETE FROM invites WHERE code='+db.raw.escape(code));
}
export { register, update, changepwd, changesk, login, updateChat, deleteVODs, getConfig, genInvite, useInvite, validInvite };

View File

@ -1,4 +1,5 @@
import * as db from "./database" import * as db from "./database";
import * as api from "./api";
import * as flags from "flags"; import * as flags from "flags";
db.init(); db.init();
@ -6,6 +7,7 @@ db.init();
flags.defineString('adduser', '', 'User to add'); flags.defineString('adduser', '', 'User to add');
flags.defineString('rmuser', '', 'User to remove'); flags.defineString('rmuser', '', 'User to remove');
flags.defineString('password', '', 'password to hash'); flags.defineString('password', '', 'password to hash');
flags.defineBoolean('invite', false, 'generate invite code');
flags.parse(); flags.parse();
@ -24,3 +26,12 @@ if(flags.get('rmuser') !== ''){
process.exit(); process.exit();
}); });
} }
if(flags.get('invite')){
var config = require("./config").config;
api.genInvite().then((r: string) => {
console.log('invite code: '+r);
console.log('Direct the user to https://'+config['satyr']['domain']+'/invite/'+r);
process.exit();
});
}

View File

@ -16,7 +16,7 @@ const config: Object = {
domain: '', domain: '',
registration: false, registration: false,
email: null, email: null,
restrictedNames: [ 'live', 'user', 'users', 'register', 'login' ], restrictedNames: [ 'live', 'user', 'users', 'register', 'login', 'invite' ],
rootredirect: '/users/live', rootredirect: '/users/live',
version: process.env.npm_package_version, version: process.env.npm_package_version,
}, localconfig['satyr']), }, localconfig['satyr']),

8
src/db/2.ts Normal file
View File

@ -0,0 +1,8 @@
import * as db from "../database";
async function run () {
await db.query('CREATE TABLE IF NOT EXISTS invites(code VARCHAR(150))');
await db.query('INSERT INTO db_meta (version) VALUES (2)');
}
export { run }

View File

@ -224,6 +224,23 @@ async function initAPI() {
}); });
}); });
app.post('/api/register', (req, res) => { app.post('/api/register', (req, res) => {
if("invite" in req.body){
api.validInvite(req.body.invite).then((v) => {
if(v){
api.register(req.body.username, req.body.password, req.body.confirm, true).then((result) => {
if(result[0]) return genToken(req.body.username).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.json(result);
api.useInvite(req.body.invite);
return;
});
res.json(result);
});
}
else res.json({error: "invalid invite code"});
});
}
else
api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => { api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
if(result[0]) return genToken(req.body.username).then((t) => { if(result[0]) return 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'});
@ -486,6 +503,12 @@ async function initSite(openReg) {
} }
else res.render('login.njk',njkconf); else res.render('login.njk',njkconf);
}); });
app.get('/invite/:code', (req, res) => {
if(tryDecode(req.cookies.Authorization)) {
res.redirect('/profile');
}
else res.render('invite.njk',Object.assign({icode: req.params.code}, njkconf));
});
app.get('/register', (req, res) => { app.get('/register', (req, res) => {
if(tryDecode(req.cookies.Authorization) || !openReg) { if(tryDecode(req.cookies.Authorization) || !openReg) {
res.redirect(njkconf.rootredirect); res.redirect(njkconf.rootredirect);

20
templates/invite.njk Normal file
View File

@ -0,0 +1,20 @@
{% extends "base.njk" %}
{% block content %}
<h3>You've been invited to {{ 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></br>
<input type="hidden" name="invite" style="min-width: 300px" value="{{icode}}"/>
<input type="submit" value="Submit">
</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>-->
{% endblock %}