1
0
Fork 0

Compare commits

...

4 Commits

13 changed files with 616 additions and 273 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Notes
.notes
# Logs
/logs/*.log
npm-debug.log*

View File

@ -4,238 +4,38 @@
* (c) Ruben Meyer <contact@rxbn.de>
*/
var path = require('path');
var fs = require('fs');
var express = require('express');
var route = express.Router();
asyncer = require('express-async-handler');
getRoutes = async () => {
let db = global['requireModule']('database');
await db.connect();
/**
* register a user; currently not implemented
* @url /register
* @method POST
*/
route.post('/register', (req, res) => {
// if registration is disabled
if(!global['app'].cfg.web.registration) {
return res.type('json').status(400).end(JSON.stringify({status: 400, message: "msg.auth.registration.deactivated"}));
} else {
// am i rite?
return res.type('json').status(200).end(JSON.stringify({}));
}
});
/**
* login a user
* @url /api/login
* @method POST
* @POST ['email', 'password']
* @TODO add new activity 'action.user.login'
*/
route.post('/login', asyncer(async (req, res) => {
// if user is logged in (existing session); FAIL
if(req.session.user) {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.logout.required'
}));
}
// check body variables
if(!req.body.email || !req.body.password) {
return res.type('json').status(401).end(JSON.stringify({
status: 401,
message: [
'msg.request.data.missing',
'msg.auth.login.failed'
]
}));
}
let email = req.body.email;
let pass = req.body.password;
// database query: get user by email
user = await db.getUser(email);
// if database error
if(user.err) {
// log error while debugging
global['logs'].debug(user.err);
// login failed because of database error
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error',
'msg.auth.login.failed'
]
}));
}
// no reply (user does not exist) or password is wrong
if(!user.reply || user.reply === null || user.reply.length == 0 || user.reply.length > 1 || !global['requireModule']('auth').validateHash(user.reply.passhash, pass)) {
return res.type('json').status(401).end(JSON.stringify({
status: 401,
message: 'msg.auth.login.failed'
}));
// do login
} else {
// add cookies; login
// new activity 'action.user.login'
// add session data
req.session.user = {
'id': user.reply._id,
'group': user.reply.group
};
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.auth.login.successful',
type: 'form' // TODO: types - { form, access_app}
}));
}
}));
/**
* apps verify token
* @url /api/authenticate
* @method POST
* @POST ['applicationId', 'applicationSecret', 'userId', 'token']
* @TODO add implementation
*/
route.post('/authenticate', (req, res) => {
// TODO: authenticate
});
/**
* cancel app request and clear it
* @url /api/cancel
* @method GET
*/
route.get('/cancel', (req, res) => {
// if user is logged in
if(req.session && req.session.user) {
req.session.appRequest = {};
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.request.operation.cancel.successful'
}));
// user isnt logged in
} else {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.login.required'
}));
}
});
/**
* redirect user to app
* @url /api/redirect
* @method GET
* @GET ['id']
*/
route.get('/redirect', asyncer(async (req, res) => {
// if user is logged in
if(req.session && req.session.user) {
// missing query data to retrieve app
if(!req.query || !req.query.id) {
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.request.data.missing'
]
}));
}
// set auth code
authCode = await db.setAuthCode({
aId: req.query.id,
uId: req.session.user.id
});
// database error
if(typeof authCode.err !== "undefined") {
global['logs'].debug(authCode.err);
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error'
]
}));
}
else if(typeof authCode.reply !== "undefined") {
// retrieve apps
apps = await db.getApps();
// database error
if(typeof apps.err !== "undefined") {
global['logs'].debug(apps.err);
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error'
]
}));
}
// for each app
apps.reply.forEach((app) => {
// if app.id is equal to queried app
if(app.id == req.query.id) {
// redirect to app
return res.redirect(app.access+"?uid="+req.session.user.id+"&token="+authCode.reply.token);
}
});
} else {
// database error
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error'
]
}));
}
// user isnt logged in
} else {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.login.required'
}));
}
}));
/**
* logout user
* @url /api/logout
* @method GET
*/
route.get('/logout', (req, res) => {
// user needs to be logged in
if(!req.session || !req.session.user) {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.login.required'
}));
// logout user
} else {
res.clearCookie('RememberMe');
req.session.destroy();
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.auth.logout.successful'
}));
}
});
// add all routes from files in api
let routesPath = path.join(global['__dirname'], '/bin/web/routes/api');
await addRoutesFromDirectory(routesPath);
return route;
};
addRoutesFromDirectory = async (filepath) => {
for await (const file of fs.readdirSync(filepath)) {
let joinedPath = path.join(filepath, file);
if(fs.statSync(joinedPath).isFile())
await addRoutesFromFile(joinedPath);
else if(fs.statSync(joinedPath).isDirectory())
await addRoutesFromDirectory(joinedPath);
}
};
addRoutesFromFile = async (filepath) => {
let file = require(filepath);
if(typeof file !== "object" || !file.hasOwnProperty("path")) return;
if(file.hasOwnProperty("all")) route.all(file.path, asyncer(file.all));
if(file.hasOwnProperty("get")) route.get(file.path, asyncer(file.get));
if(file.hasOwnProperty("post")) route.post(file.path, asyncer(file.post));
};
module.exports = {
getRoutes: getRoutes
};

View File

@ -0,0 +1,13 @@
module.exports = {
path: "/authenticate",
/**
* apps verify token
* @url /api/authenticate
* @method POST
* @POST ['applicationId', 'applicationSecret', 'userId', 'token']
* @TODO add implementation
*/
post: async (req, res) => {
return res.end();
}
};

View File

@ -0,0 +1,26 @@
module.exports = {
path: "/cancel",
/**
* cancel app request and clear it
* @url /api/cancel
* @method GET
*/
get: (req, res) => {
// if user is logged in
if(req.session && req.session.user) {
req.session.appRequest = {};
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.request.operation.cancel.successful'
}));
// user isnt logged in
} else {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.login.required'
}));
}
}
};

View File

@ -0,0 +1,78 @@
var sanitize = require('mongo-sanitize');
let db = global['requireModule']('database');
module.exports = {
path: "/login",
/**
* login a user
* @url /api/login
* @method POST
* @POST ['email', 'password']
* @TODO add new activity 'action.user.login'
*/
post: async (req, res) => {
// if user is logged in (existing session); FAIL
if(req.session.user) {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.logout.required'
}));
}
// check body variables
if(!req.body.email || !req.body.password) {
return res.type('json').status(401).end(JSON.stringify({
status: 401,
message: [
'msg.request.data.missing',
'msg.auth.login.failed'
]
}));
}
let email = sanitize(req.body.email);
let pass = sanitize(req.body.password);
// database query: get user by email
user = await db.getUser(email);
// if database error
if(user.err) {
// log error while debugging
global['logs'].debug(user.err);
// login failed because of database error
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error',
'msg.auth.login.failed'
]
}));
}
// no reply (user does not exist) or password is wrong
if(!user.reply || user.reply === null || user.reply.length == 0 || user.reply.length > 1 || !global['requireModule']('auth').validateHash(user.reply.passhash, pass)) {
return res.type('json').status(401).end(JSON.stringify({
status: 401,
message: 'msg.auth.login.failed'
}));
// do login
} else {
// add cookies; login
// new activity 'action.user.login'
// add session data
req.session.user = {
'id': user.reply._id,
'group': user.reply.group
};
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.auth.login.successful',
type: 'form' // TODO: types - { form, access_app}
}));
}
}
};

View File

@ -0,0 +1,25 @@
module.exports = {
path: "/logout",
/**
* logout user
* @url /api/logout
* @method GET
*/
get: (req, res) => {
// user needs to be logged in
if(!req.session || !req.session.user) {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.login.required'
}));
// logout user
} else {
res.clearCookie('RememberMe');
req.session.destroy();
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.auth.logout.successful'
}));
}
}
};

View File

@ -0,0 +1,78 @@
let db = global['requireModule']('database');
module.exports = {
path: "/redirect",
/**
* redirect user to app
* @url /api/redirect
* @method GET
* @GET ['id']
*/
get: async (req, res) => {
// if user is logged in
if(req.session && req.session.user) {
// missing query data to retrieve app
if(!req.query || !req.query.id) {
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.request.data.missing'
]
}));
}
// set auth code
authCode = await db.setAuthCode({
aId: req.query.id,
uId: req.session.user.id
});
// database error
if(typeof authCode.err !== "undefined") {
global['logs'].debug(authCode.err);
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error'
]
}));
}
else if(typeof authCode.reply !== "undefined") {
// retrieve apps
apps = await db.getApps();
// database error
if(typeof apps.err !== "undefined") {
global['logs'].debug(apps.err);
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error'
]
}));
}
// for each app
apps.reply.forEach((app) => {
// if app.id is equal to queried app
if(app.id == req.query.id) {
// redirect to app
return res.redirect(app.access+"?uid="+req.session.user.id+"&token="+authCode.reply.token);
}
});
} else {
// database error
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: [
'msg.database.error'
]
}));
}
// user isnt logged in
} else {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.login.required'
}));
}
}
};

View File

@ -0,0 +1,17 @@
module.exports = {
path: "/register",
/**
* register a user; currently not implemented
* @url /register
* @method POST
*/
post: async (req, res) => {
// if registration is disabled
if(!global['gds'].cfg.web.registration) {
return res.type('json').status(400).end(JSON.stringify({status: 400, message: "msg.auth.registration.deactivated"}));
} else {
// am i rite?
return res.type('json').status(200).end(JSON.stringify({}));
}
}
};

View File

@ -0,0 +1,95 @@
let sanitize = require('mongo-sanitize');
let db = global['requireModule']('database');
let crypto = require('crypto');
let auth = global['requireModule']('auth');
module.exports = {
path: "/settings",
/**
* update user
* @TODO add implementation
* @url /api/settings
* @method POST
* @POST ['email', 'password', 'repassword', 'mfa']
*/
post: async (req, res) => {
// if user is not logged in; FAIL
if(!req.session || !req.session.user) {
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.logout.required'
}));
}
// check body variables
if(
!(
(req.body.email) ||
(req.body.password && req.body.repassword) ||
(req.body.mfa)
)
) {
return res.type('json').status(401).end(JSON.stringify({
status: 401,
message: 'msg.request.data.missing'
}));
}
user = await db.getUser(req.session.user.id);
// if database error
if(user.err) {
// log error while debugging
global['logs'].debug(user.err);
// query failed because of database error
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: 'msg.database.error'
}));
}
obj = {};
if(req.body.email) obj.email = sanitize(req.body.email);
if(req.body.password && req.body.repassword) {
password = sanitize(req.body.password);
repassword = sanitize(req.body.repassword);
if(password.length == repassword.length && crypto.timingSafeEqual(
Buffer.from(password, 'hex'),
Buffer.from(repassword, 'hex')
)) {
obj.passhash = auth.generateHash(password);
} else {
return res.type('json').status(400).end(JSON.stringify({status: 400, message: "msg.request.data.missing"}));
}
}
// empty obj, do not update
if(Object.keys(obj).length === 0 && obj.constructor === Object) {
return res.type('json').status(400).end(JSON.stringify({status: 400, message: "msg.request.data.missing"}));
}
update = await db.updateUser(user.reply._id, obj);
// if database error
if(update.err) {
// log error while debugging
global['logs'].debug(update.err);
// update failed because of database error
return res.type('json').status(500).end(JSON.stringify({
status: 500,
message: 'msg.database.error'
}));
}
else if(update.reply) {
console.log(obj);
return res.type('json').end(JSON.stringify({
status: 200,
message: 'msg.settings.update.successful'
}));
}
return res.type('json').end(JSON.stringify({
status: 401,
message: 'msg.auth.logout.required'
}));
}
};

View File

@ -95,6 +95,29 @@ let getRoutes = async () => {
}
}));
/**
* settings page
* @url /settings
* @method GET
*/
route.get('/settings', asyncer(async (req, res, next) => {
if(req.session && req.session.user) {
// query user
user = await db.getUser(req.session.user.id);
if(user.reply) {
group = await db.getGroup(user.reply.group);
return res.render('settings', {
session: req.session,
cfg: cfg,
user: user.reply,
group: group.reply
});
}
else
return res.render('error/404');
} else {
return res.render('login', { session: req.session, cfg: cfg });
}
}));
@ -108,7 +131,28 @@ let getRoutes = async () => {
if(req.path.startsWith('/api'))
return next();
if(req.path == "/request") return res.render('error/404');
let obj = {
session: req.session,
cfg: cfg
};
if(req.session && req.session.user) {
// query user
userQuery = await db.getUser(req.session.user.id);
if(userQuery.reply) {
groupQuery = await db.getGroup(userQuery.reply.group);
obj.user = userQuery.reply;
obj.group = groupQuery.reply;
}
}
if(req.path == "/request") {
if(req.query.appId) {
if(req.query.appId && typeof req.query.appId == "string") {
req.session.appRequest.appId = req.query.appId;
}
}
return res.redirect('/authenticate');
}
let pathRules = await db.getPathRules();
@ -132,30 +176,18 @@ let getRoutes = async () => {
let regex = new RegExp(rule.expression, "g");
if(regex.test(req.path)) {
if(rule.type == "404") {
return res.status(404).render('error/404', {
error_code: 404,
error_msg: 'msg.request.file.not_found',
session: req.session,
cfg: cfg
});
obj.error_code = 404;
obj.error_msg = 'msg.request.file.not_found';
return res.status(404).render('error/404', obj);
} else if(rule.type == "missing_permission") {
return res.status(401).render('error/permission', {
error_code: 401,
session: req.session,
cfg: cfg
});
obj.error_code = 401;
return res.status(401).render('error/permission', obj);
} else if(rule.type == "login" && (!req.session || !req.session.user)) {
return res.status(401).render('error/login', {
error_code: 401,
session: req.session,
cfg: cfg
});
obj.error_code = 401;
return res.status(401).render('error/login', obj);
} else {
return res.status(401).render('error/error', {
error_code: 401,
session: req.session,
cfg: cfg
});
obj.error_code = 401;
return res.status(401).render('error/error', obj);
}
}
}
@ -165,18 +197,12 @@ let getRoutes = async () => {
let dir = global['__dirname'] + '/bin/web/views';
let path_j = path.join(dir, req.path.toLowerCase());
if(fs.existsSync(path_j+'.pug')) {
return res.render(req.path.replace(/^\//, ''), {
session: req.session,
cfg: cfg
});
return res.render(req.path.replace(/^\//, ''), obj);
} else {
global['logs'].info("[web] (404) path not found: "+req.path);
return res.status(404).render('error/404', {
error_code: 404,
error_msg: 'msg.request.file.not_found',
session: req.session,
cfg: cfg
});
obj.error_code = 404;
obj.error_msg = 'msg.request.file.not_found';
return res.status(404).render('error/404', obj);
}
}));

View File

@ -1,11 +1,9 @@
extends blocks/layout.pug
append var
if(session && !session.user)
- var breadcrumb = {0: {"name": cfg.app.name, "href": "/"}, 1: {"name": "Forgot your password?", "active": true}};
- var title = "Reset password";
append content
if(session && !session.user)
.uk-flex.uk-margin-medium-top.uk-margin-medium-bottom
div(class="uk-width-auto uk-width-1-4@s")
.uk-flex.uk-flex-auto.uk-flex-column.uk-flex-center.uk-margin-left.uk-margin-right
@ -17,7 +15,3 @@ append content
input.uk-input#login_user(type="text", placeholder="tetrahedron")
a(href="#").uk-button.uk-button-default Reset password
div(class="uk-width-auto uk-width-1-4@s")
else
append var
- overwrite_vars = (session && session.user) ? true : false;
include blocks/error/permission.pug

View File

@ -0,0 +1,93 @@
extends blocks/layout.pug
append var
if(session && session.user)
- var breadcrumb = {0: {"name": cfg.app.name, "href": "/"}, 1: {"name": "Settings", "active": true}};
- var title = "Settings";
mixin setting(name, id, inputId, hidden, inputType, inputValue, inputPlaceholder, inputIcon, inputDisabled)
.uk-margin(id=id)
label.uk-form-label(for=inputId)=name
if(inputIcon)
.uk-inline.uk-width-1-1
span.uk-form-icon(uk-icon="icon: "+inputIcon)
if(inputDisabled == "disabled")
input.uk-input(id=inputId, type=inputType, value=inputValue, placeholder=inputPlaceholder, disabled)
else
input.uk-input(id=inputId, type=inputType, value=inputValue, placeholder=inputPlaceholder)
else
input.uk-input(id=inputId, type=inputType, value=inputValue, placeholder=inputPlaceholder)
//-.uk-card.uk-card-default
.uk-card-header.uk-card-primary
h3.uk-card-title=name
.uk-card-body
p=id
.uk-card-footer.uk-flex.uk-flex-right
a.uk-button.uk-button-default.uk-button-primary(href="/api/redirect?id="+id) Login
mixin settings()
form(class="uk-child-width-expand uk-margin-bottom", uk-grid)
fieldset.uk-fieldset
h1 Settings
h2 Profile
+setting('', 'lorem-ipsum', 'form-profile-username', 'false', 'text', user.nickname, '', 'user', 'disabled')
+setting('', 'lorem-ipsum', 'form-profile-email', 'false', 'text', user.email, 'john.doe@example.com', 'mail')
div(class="uk-flex uk-flex-between uk-margin-bottom", uk-grid)
div(class="uk-width-2-3@m")
#profile_msg.uk-alert(data-uk-alert).uk-hidden
a.uk-close-alt.uk-alert-close(href="#")
p
div
a(onclick="saveProfile(event)").uk-button.uk-button-primary Save
h2 Password
+setting('Password', 'form-password', 'form-security-password', 'false', 'password', '', '')
+setting('Reenter Password', 'form-repassword', 'form-security-repassword', 'false', 'password', '', '')
div(class="uk-flex uk-flex-between uk-margin-bottom", uk-grid)
div(class="uk-width-2-3@m")
#password_msg.uk-alert(data-uk-alert).uk-hidden
a.uk-close-alt.uk-alert-close(href="#")
p
div
a(onclick="savePassword(event)").uk-button.uk-button-primary Save
div(class="uk-flex uk-flex-between uk-margin-bottom", uk-grid)
div(class="uk-width-2-3@m uk-flex-column")
h2 Multifactor Authentication
#mfa_msg.uk-alert(data-uk-alert).uk-hidden
a.uk-close-alt.uk-alert-close(href="#")
p
div
if(user.mfa && user.mfa.active)
a(onclick="switchMFA(event)").uk-button.uk-button-primary Disable
if(!user.mfa || !user.mfa.active)
a(onclick="switchMFA(event)").uk-button.uk-button-primary Enable
if(user.mfa && user.mfa.active)
div(class="uk-flex uk-flex-between uk-margin-bottom", uk-grid)
div(class="uk-width-1-1")
if(user.mfa.data)
//- sort by number
- user.mfa.data.sort(function(a, b) {return a.no - b.no; })
each option in user.mfa.data
.uk-card.uk-card-default.uk-card-body.uk-margin-bottom.uk-width-1-1
.uk-flex.uk-flex-between
div
h3.uk-card-title= "Authentication Layer: "+option.no
div
p= "type: "
span.uk-badge= option.type
.uk-flex.uk-flex-between
div
a.uk-button.uk-button-default(onclick="showMFASecret(event)", id="secret.show."+option.type+"."+option.no) Show/Hide Secret
div
a.uk-button.uk-button-primary(onclick="removeMFA(event)", id="secret.remove."+option.type+"."+option.no) remove Layer
p(class="secret uk-hidden", id="secret."+option.type+"."+option.no, data-secret=option.data)= "SECRET: " + option.data
append content
if(session && session.user)
.uk-container
+settings()
else
include blocks/login.pug

View File

@ -70,6 +70,101 @@ function cancelRequest() {
};
}
// '/settings', shows MFA Secret on button click
function showMFASecret(e) {
let secret = document.getElementById(e.target.id).parentNode.parentNode.parentNode.getElementsByClassName("secret")[0];
if(!secret.classList.contains("uk-hidden")) {
secret.classList.add("uk-hidden");
} else {
secret.classList.remove("uk-hidden");
let secretData = secret.dataset.secret;
let options = secret.id.split(".");
// @TODO
switch (options[1]) {
case "HOTP":
break;
case "TOTP":
break;
case "WebAuthn":
break;
default:
}
}
return false;
}
// @TODO
// '/settings', removes MFA Layer
function removeMFA(e) {
return false;
}
// '/settings', saves Profile
function saveProfile(e) {
let email = document.getElementById("form-profile-email").value;
let data = {
"email": email
};
let ajax = new XMLHttpRequest();
ajax.open("POST", "/api/settings", true);
ajax.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
ajax.send(JSON.stringify(data));
ajax.onload = () => {
let json = JSON.parse(ajax.responseText);
let box = document.getElementById("profile_msg");
box.classList.remove("uk-hidden");
box.classList.remove("uk-alert-success");
box.classList.remove("uk-alert-danger");
if(json.message && json.message == "msg.settings.update.successful") {
box.classList.add("uk-alert-success");
box.getElementsByTagName("p")[0].innerHTML = "Update was successfully";
} else if(json.message) {
box.classList.add("uk-alert-danger");
box.getElementsByTagName("p")[0].innerHTML = "Update failed";
}
};
return false;
}
// '/settings', saves Password
function savePassword(e) {
let pass = document.getElementById("form-security-password").value;
let repass = document.getElementById("form-security-repassword").value;
let data = {
"password": pass,
"repassword": repass
};
let ajax = new XMLHttpRequest();
ajax.open("POST", "/api/settings", true);
ajax.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
ajax.send(JSON.stringify(data));
ajax.onload = () => {
let json = JSON.parse(ajax.responseText);
let box = document.getElementById("password_msg");
box.classList.remove("uk-hidden");
box.classList.remove("uk-alert-success");
box.classList.remove("uk-alert-danger");
if(json.message && json.message == "msg.settings.update.successful") {
box.classList.add("uk-alert-success");
box.getElementsByTagName("p")[0].innerHTML = "Update was successfully";
} else if(json.message) {
box.classList.add("uk-alert-danger");
box.getElementsByTagName("p")[0].innerHTML = "Update failed";
}
};
return false;
}
// @url: https://stackoverflow.com/a/901144
function getParameterByName(name) {
let url = window.location.href;