Updating a bunch of stuff.

This version has partially support for TwoFactorAuthentication.
This commit is contained in:
Fabian Stamm 2019-08-06 17:06:22 +02:00
parent d98588d1c3
commit 11f460406b
22 changed files with 2224 additions and 1812 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/lib/index.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}]
}

View File

@ -1,37 +1,39 @@
{ {
"User not found": "Benutzer nicht gefunden", "User not found": "Benutzer nicht gefunden",
"Password or username wrong": "Passwort oder Benutzername falsch", "Password or username wrong": "Passwort oder Benutzername falsch",
"Authorize %s": "Authorize %s", "Authorize %s": "Authorize %s",
"Login": "Einloggen", "Login": "Einloggen",
"You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.", "You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.",
"Username or Email": "Benutzername oder Email", "Username or Email": "Benutzername oder Email",
"Password": "Passwort", "Password": "Passwort",
"Next": "Weiter", "Next": "Weiter",
"Register": "Registrieren", "Register": "Registrieren",
"Mail": "Mail", "Mail": "Mail",
"Repeat Password": "Passwort wiederholen", "Repeat Password": "Passwort wiederholen",
"Username": "Benutzername", "Username": "Benutzername",
"Name": "Name", "Name": "Name",
"Registration code": "Registrierungs Schlüssel", "Registration code": "Registrierungs Schlüssel",
"You need to select one of the options": "Du musst eine der Optionen auswälen", "You need to select one of the options": "Du musst eine der Optionen auswälen",
"Male": "Mann", "Male": "Mann",
"Female": "Frau", "Female": "Frau",
"Other": "Anderes", "Other": "Anderes",
"Registration code required": "Registrierungs Schlüssel benötigt", "Registration code required": "Registrierungs Schlüssel benötigt",
"Username required": "Benutzername benötigt", "Username required": "Benutzername benötigt",
"Name required": "Name benötigt", "Name required": "Name benötigt",
"Mail required": "Mail benötigt", "Mail required": "Mail benötigt",
"The passwords do not match": "Die Passwörter stimmen nicht überein", "The passwords do not match": "Die Passwörter stimmen nicht überein",
"Password is required": "Password benötigt", "Password is required": "Password benötigt",
"Invalid registration code": "Ungültiger Registrierungs Schlüssel", "Invalid registration code": "Ungültiger Registrierungs Schlüssel",
"Username taken": "Benutzername nicht verfügbar", "Username taken": "Benutzername nicht verfügbar",
"Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden", "Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden",
"Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet", "Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet",
"Administration": "Administration", "Administration": "Administration",
"Field {{field}} is not defined": "Feld {{field}} fehlt", "Field {{field}} is not defined": "Feld {{field}} fehlt",
"Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Typ. Es sollte vom Typ {{type}} sein", "Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Typ. Es sollte vom Typ {{type}} sein",
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen", "Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
"Invalid token": "Ungültiger Token", "Invalid token": "Ungültiger Token",
"By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen.", "By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen.",
"User": "User" "User": "User",
"No special token": "No special token",
"Login token invalid": "Login token invalid"
} }

View File

@ -3,5 +3,11 @@
"Username or Email": "Username or Email", "Username or Email": "Username or Email",
"Password": "Password", "Password": "Password",
"Next": "Next", "Next": "Next",
"Invalid code": "Invalid code" "Invalid code": "Invalid code",
"You are not logged in or your login is expired": "You are not logged in or your login is expired",
"User not found": "User not found",
"No special token": "No special token",
"You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)",
"Special token invalid": "Special token invalid",
"You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)"
} }

1009
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,44 +17,44 @@
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.17.0", "@types/body-parser": "^1.17.0",
"@types/compression": "^0.0.36", "@types/compression": "^1.0.0",
"@types/cookie-parser": "^1.4.1", "@types/cookie-parser": "^1.4.1",
"@types/dotenv": "^6.1.0", "@types/dotenv": "^6.1.1",
"@types/express": "^4.16.1", "@types/express": "^4.17.0",
"@types/i18n": "^0.8.5", "@types/i18n": "^0.8.5",
"@types/ini": "^1.3.30", "@types/ini": "^1.3.30",
"@types/jsonwebtoken": "^8.3.2", "@types/jsonwebtoken": "^8.3.2",
"@types/mongodb": "^3.1.22", "@types/mongodb": "^3.1.31",
"@types/node": "^11.11.3", "@types/node": "^12.6.9",
"@types/node-rsa": "^1.0.0", "@types/node-rsa": "^1.0.0",
"@types/qrcode": "^1.3.1", "@types/qrcode": "^1.3.3",
"@types/speakeasy": "^2.0.4", "@types/speakeasy": "^2.0.5",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.5",
"apidoc": "^0.17.7", "apidoc": "^0.17.7",
"concurrently": "^4.1.0", "concurrently": "^4.1.1",
"nodemon": "^1.18.10", "nodemon": "^1.19.1",
"typescript": "^3.3.3333" "speakeasy": "^2.0.0",
"typescript": "^3.5.3"
}, },
"dependencies": { "dependencies": {
"@hibas123/nodelogging": "^1.3.21", "@hibas123/nodelogging": "^2.1.0",
"@hibas123/nodeloggingserver_client": "^1.1.2", "@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/safe_mongo": "^1.5.3", "@hibas123/safe_mongo": "^1.6.1",
"body-parser": "^1.18.3", "body-parser": "^1.19.0",
"compression": "^1.7.3", "compression": "^1.7.4",
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^7.0.0", "dotenv": "^8.0.0",
"express": "^4.16.4", "express": "^4.17.1",
"handlebars": "^4.1.0", "handlebars": "^4.1.2",
"i18n": "^0.8.3", "i18n": "^0.8.3",
"ini": "^1.3.5", "ini": "^1.3.5",
"jsonwebtoken": "^8.5.0", "jsonwebtoken": "^8.5.1",
"moment": "^2.24.0", "moment": "^2.24.0",
"mongodb": "^3.1.13", "mongodb": "^3.2.7",
"node-rsa": "^1.0.5", "node-rsa": "^1.0.5",
"qrcode": "^1.3.3", "qrcode": "^1.4.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"speakeasy": "^2.0.0",
"u2f": "^0.1.3", "u2f": "^0.1.3",
"uuid": "^3.3.2" "uuid": "^3.3.2"
} }

View File

@ -1,30 +1,40 @@
import { Request, Response, Router } from "express" import { Request, Response, Router } from "express"
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client"; import { GetClientAuthMiddleware } from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user"; import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys"; import { createJWT } from "../../keys";
import Client from "../../models/client";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
const ClientRouter = Router();
/**
* @api {get} /client/user const ClientRouter = Router();
* @apiParam {String} redirect_uri URL to redirect to on success
* @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter /**
* * @api {get} /client/user
* @apiName ClientUser *
* @apiGroup client * @apiDescription Can be used for simple authentication of user. It will redirect the user to the redirect URI with a very short lived jwt.
* *
* @apiPermission user_client Requires ClientID and Authenticated User * @apiParam {String} redirect_uri URL to redirect to on success
*/ * @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter
ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(false, false), async (req: Request, res: Response) => { *
let { redirect_uri, state } = req.query; * @apiName ClientUser
let jwt = await createJWT({ * @apiGroup client
client: req.client.client_id, *
uid: req.user.uid, * @apiPermission user_client Requires ClientID and Authenticated User
username: req.user.username, */
state: state ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(false, false), async (req: Request, res: Response) => {
}, 30); //after 30 seconds this token is invalid let { redirect_uri, state } = req.query;
res.redirect(redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : ""));
})); if (redirect_uri !== req.client.redirect_url)
throw new RequestError("Invalid redirect URI", HttpStatusCode.BAD_REQUEST);
let jwt = await createJWT({
client: req.client.client_id,
uid: req.user.uid,
username: req.user.username,
state: state
}, 30); //after 30 seconds this token is invalid
res.redirect(redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : ""));
}));
export default ClientRouter; export default ClientRouter;

View File

@ -1,74 +1,79 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import LoginToken, { CheckToken } from "../../models/login_token"; import LoginToken, { CheckToken } from "../../models/login_token";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user"; import User from "../../models/user";
import promiseMiddleware from "../../helper/promiseMiddleware"; import promiseMiddleware from "../../helper/promiseMiddleware";
class Invalid extends Error { } class Invalid extends Error { }
/** /**
* Returns customized Middleware function, that could also be called directly * Returns customized Middleware function, that could also be called directly
* by code and will return true or false depending on the token. In the false * by code and will return true or false depending on the token. In the false
* case it will also send error and redirect if json is not set * case it will also send error and redirect if json is not set
* @param json Checks if requests wants an json or html for returning errors * @param json Default false. Checks if requests wants an json or html for returning errors
* @param redirect_uri Sets the uri to redirect, if json is not set and user not logged in * @param special_required Default false. If true, a special token is required
*/ * @param redirect_uri Default current uri. Sets the uri to redirect, if json is not set and user not logged in
export function GetUserMiddleware(json = false, special_required: boolean = false, redirect_uri?: string, validated = true) { * @param validated Default true. If false, the token must not be validated
return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) { */
const invalid = () => { export function GetUserMiddleware(json = false, special_required: boolean = false, redirect_uri?: string, validated = true) {
throw new Invalid(); return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) {
} const invalid = (message: string) => {
try { throw new Invalid(req.__(message));
let { login, special } = req.cookies }
if (!login) invalid() try {
let { login, special } = req.cookies
let token = await LoginToken.findOne({ token: login, valid: true }) if (!login) {
if (!await CheckToken(token, validated)) invalid(); login = req.query.login;
special = req.query.special;
let user = await User.findById(token.user); }
if (!user) { if (!login) invalid("No login token")
token.valid = false; if (!special && special_required) invalid("No special token")
await LoginToken.save(token);
invalid(); let token = await LoginToken.findOne({ token: login, valid: true })
} if (!await CheckToken(token, validated)) invalid("Login token invalid");
let special_token; let user = await User.findById(token.user);
if (special) { if (!user) {
Logging.debug("Special found") token.valid = false;
special_token = await LoginToken.findOne({ token: special, special: true, valid: true, user: token.user }) await LoginToken.save(token);
if (!await CheckToken(special_token, validated)) invalid("Login token invalid");
invalid(); }
req.special = true;
} let special_token;
if (special) {
if (special_required && !req.special) invalid(); Logging.debug("Special found")
special_token = await LoginToken.findOne({ token: special, special: true, valid: true, user: token.user })
req.user = user if (!await CheckToken(special_token, validated))
req.isAdmin = user.admin; invalid("Special token invalid");
req.token = { req.special = true;
login: token, }
special: special_token
} req.user = user
req.isAdmin = user.admin;
if (next) req.token = {
next() login: token,
return true; special: special_token
} catch (e) { }
if (e instanceof Invalid) {
if (req.method === "GET" && !json) { if (next)
res.status(HttpStatusCode.UNAUTHORIZED) next()
res.redirect("/login?base64=true&state=" + Buffer.from(redirect_uri ? redirect_uri : req.originalUrl).toString("base64")) return true;
} else { } catch (e) {
throw new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED) if (e instanceof Invalid) {
} if (req.method === "GET" && !json) {
} else { res.status(HttpStatusCode.UNAUTHORIZED)
if (next) next(e); res.redirect("/login?base64=true&state=" + Buffer.from(redirect_uri ? redirect_uri : req.originalUrl).toString("base64"))
else throw e; } else {
} throw new RequestError(req.__("You are not logged in or your login is expired" + ` (${e.message})`), HttpStatusCode.UNAUTHORIZED, undefined, { auth: true })
return false; }
} } else {
}); if (next) next(e);
} else throw e;
}
return false;
}
});
}
export const UserMiddleware = GetUserMiddleware(); export const UserMiddleware = GetUserMiddleware();

16
src/api/user/account.ts Normal file
View File

@ -0,0 +1,16 @@
import { Request, Response } from "express";
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import LoginToken, { CheckToken } from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
export const GetAccount = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
let user = {
id: req.user.uid,
name: req.user.name,
username: req.user.username,
birthday: req.user.birthday,
gender: req.user.gender,
};
res.json({ user });
});

View File

@ -1,95 +1,113 @@
import { Router } from "express"; import { Router } from "express";
import Register from "./register"; import Register from "./register";
import Login from "./login"; import Login from "./login";
import TwoFactorRoute from "./twofactor"; import TwoFactorRoute from "./twofactor";
import { GetToken, DeleteToken } from "./token"; import { GetToken, DeleteToken } from "./token";
import { GetAccount } from "./account";
const UserRoute: Router = Router();
const UserRoute: Router = Router();
/**
* @api {post} /user/register /**
* @apiName UserRegister * @api {post} /user/register
* * @apiName UserRegister
* @apiGroup user *
* @apiPermission none * @apiGroup user
* * @apiPermission none
* @apiParam {String} mail EMail linked to this Account *
* @apiParam {String} username The new Username * @apiParam {String} mail EMail linked to this Account
* @apiParam {String} password Password hashed and salted like specification * @apiParam {String} username The new Username
* @apiParam {String} salt The Salt used for password hashing * @apiParam {String} password Password hashed and salted like specification
* @apiParam {String} regcode The regcode, that should be used * @apiParam {String} salt The Salt used for password hashing
* @apiParam {String} gender Gender can be: "male", "female", "other", "none" * @apiParam {String} regcode The regcode, that should be used
* @apiParam {String} name The real name of the User * @apiParam {String} gender Gender can be: "male", "female", "other", "none"
* * @apiParam {String} name The real name of the User
* @apiSuccess {Boolean} success *
* * @apiSuccess {Boolean} success
* @apiErrorExample {Object} Error-Response: *
{ * @apiErrorExample {Object} Error-Response:
error: [ {
{ error: [
message: "Some Error", {
field: "username" message: "Some Error",
} field: "username"
], }
status: 400 ],
} status: 400
*/ }
UserRoute.post("/register", Register); */
UserRoute.post("/register", Register);
/**
* @api {post} /user/login?type=:type /**
* @apiName UserLogin * @api {post} /user/login?type=:type
* * @apiName UserLogin
* @apiParam {String} type Type could be either "username" or "password" *
* * @apiParam {String} type Type could be either "username" or "password"
* @apiGroup user *
* @apiPermission none * @apiGroup user
* * @apiPermission none
* @apiParam {String} username Username (either username or uid required) *
* @apiParam {String} uid (either username or uid required) * @apiParam {String} username Username (either username or uid required)
* @apiParam {String} password Password hashed and salted like specification (only on type password) * @apiParam {String} uid (either username or uid required)
* * @apiParam {String} password Password hashed and salted like specification (only on type password)
* @apiSuccess {String} uid On type = "username" *
* @apiSuccess {String} salt On type = "username" * @apiSuccess {String} uid On type = "username"
* * @apiSuccess {String} salt On type = "username"
* @apiSuccess {String} login On type = "password". Login Token *
* @apiSuccess {String} special On type = "password". Special Token * @apiSuccess {String} login On type = "password". Login Token
* @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required * @apiSuccess {String} special On type = "password". Special Token
* @apiSuccess {String} tfa.id The ID of the TFA Method * @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required
* @apiSuccess {String} tfa.name The name of the TFA Method * @apiSuccess {String} tfa.id The ID of the TFA Method
* @apiSuccess {String} tfa.type The type of the TFA Method * @apiSuccess {String} tfa.name The name of the TFA Method
*/ * @apiSuccess {String} tfa.type The type of the TFA Method
UserRoute.post("/login", Login) */
UserRoute.use("/twofactor", TwoFactorRoute); UserRoute.post("/login", Login)
UserRoute.use("/twofactor", TwoFactorRoute);
/**
* @api {get} /user/token /**
* @apiName UserGetToken * @api {get} /user/token
* * @apiName UserGetToken
* @apiGroup user *
* @apiPermission user * @apiGroup user
* * @apiPermission user
* @apiSuccess {Object[]} token *
* @apiSuccess {String} token.id The Token ID * @apiSuccess {Object[]} token
* @apiSuccess {String} token.special Identifies Special Token * @apiSuccess {String} token.id The Token ID
* @apiSuccess {String} token.ip IP the token was optained from * @apiSuccess {String} token.special Identifies Special Token
* @apiSuccess {String} token.browser The Browser the token was optained from (User Agent) * @apiSuccess {String} token.ip IP the token was optained from
* @apiSuccess {Boolean} token.isthis Shows if it is token used by this session * @apiSuccess {String} token.browser The Browser the token was optained from (User Agent)
*/ * @apiSuccess {Boolean} token.isthis Shows if it is token used by this session
UserRoute.get("/token", GetToken); */
UserRoute.get("/token", GetToken);
/**
* @api {delete} /user/token/:id /**
* @apiParam {String} id The id of the token to be deleted * @api {delete} /user/token/:id
* * @apiParam {String} id The id of the token to be deleted
* @apiName UserDeleteToken *
* * @apiName UserDeleteToken
* @apiParam {String} type Type could be either "username" or "password" *
* *
* @apiGroup user * @apiGroup user
* @apiPermission user * @apiPermission user
* *
* @apiSuccess {Boolean} success * @apiSuccess {Boolean} success
*/ */
UserRoute.delete("/token/:id", DeleteToken); UserRoute.delete("/token/:id", DeleteToken);
/**
* @api {delete} /user/account
* @apiName UserGetAccount
*
* @apiGroup user
* @apiPermission user
*
* @apiSuccess {Boolean} success
* @apiSuccess {Object[]} user
* @apiSuccess {String} user.id User ID
* @apiSuccess {String} token.name Full name of the user
* @apiSuccess {String} token.username Username of user
* @apiSuccess {Date} token.birthday Birthday
* @apiSuccess {Number} token.gender Gender of user (none = 0, male = 1, female = 2, other = 3)
*/
UserRoute.get("/account", GetAccount);
export default UserRoute; export default UserRoute;

View File

@ -1,98 +1,96 @@
import { Request, Response } from "express" import { Request, Response } from "express"
import User, { IUser } from "../../models/user"; import User, { IUser } from "../../models/user";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import moment = require("moment"); import moment = require("moment");
import LoginToken from "../../models/login_token"; import LoginToken from "../../models/login_token";
import promiseMiddleware from "../../helper/promiseMiddleware"; import promiseMiddleware from "../../helper/promiseMiddleware";
import TwoFactor from "../../models/twofactor"; import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
const Login = promiseMiddleware(async (req: Request, res: Response) => { const Login = promiseMiddleware(async (req: Request, res: Response) => {
let type = req.query.type; let type = req.query.type;
if (type === "username") { if (type === "username") {
let { username, uid } = req.query; let { username, uid } = req.query;
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid }); let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid });
if (!user) { if (!user) {
res.json({ error: req.__("User not found") }) res.json({ error: req.__("User not found") })
} else { } else {
res.json({ salt: user.salt, uid: user.uid }); res.json({ salt: user.salt, uid: user.uid });
} }
return; return;
} else if (type === "password") { } else if (type === "password") {
const sendToken = async (user: IUser, tfa?: any[]) => { const sendToken = async (user: IUser, tfa?: any[]) => {
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
let client = { let client = {
ip: Array.isArray(ip) ? ip[0] : ip, ip: Array.isArray(ip) ? ip[0] : ip,
browser: req.headers["user-agent"] browser: req.headers["user-agent"]
} }
let token_str = randomBytes(16).toString("hex"); let token_str = randomBytes(16).toString("hex");
let tfa_exp = moment().add(5, "minutes").toDate() let tfa_exp = moment().add(5, "minutes").toDate()
let token_exp = moment().add(6, "months").toDate() let token_exp = moment().add(6, "months").toDate()
let token = LoginToken.new({ let token = LoginToken.new({
token: token_str, token: token_str,
valid: true, valid: true,
validTill: tfa ? tfa_exp : token_exp, validTill: tfa ? tfa_exp : token_exp,
user: user._id, user: user._id,
validated: tfa ? false : true, validated: tfa ? false : true,
...client ...client
}); });
await LoginToken.save(token); await LoginToken.save(token);
let special_str = randomBytes(24).toString("hex"); let special_str = randomBytes(24).toString("hex");
let special_exp = moment().add(30, "minutes").toDate() let special_exp = moment().add(30, "minutes").toDate()
let special = LoginToken.new({ let special = LoginToken.new({
token: special_str, token: special_str,
valid: true, valid: true,
validTill: tfa ? tfa_exp : special_exp, validTill: tfa ? tfa_exp : special_exp,
special: true, special: true,
user: user._id, user: user._id,
validated: tfa ? false : true, validated: tfa ? false : true,
...client ...client
}); });
await LoginToken.save(special); await LoginToken.save(special);
res.json({ res.json({
login: { token: token_str, expires: token.validTill.toUTCString() }, login: { token: token_str, expires: token.validTill.toUTCString() },
special: { token: special_str, expires: special.validTill.toUTCString() }, special: { token: special_str, expires: special.validTill.toUTCString() },
tfa tfa
}); });
} }
let { username, password, uid } = req.body;
let { username, password, uid } = req.body; let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
if (!user) {
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid }) res.json({ error: req.__("User not found") })
if (!user) { } else {
res.json({ error: req.__("User not found") }) if (user.password !== password) {
} else { res.json({ error: req.__("Password or username wrong") })
if (user.password !== password) { } else {
res.json({ error: req.__("Password or username wrong") }) let twofactor = await TwoFactor.find({ user: user._id, valid: true })
} else { let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
let twofactor = await TwoFactor.find({ user: user._id, valid: true }) await Promise.all(expired.map(e => {
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false) e.valid = false;
await Promise.all(expired.map(e => { return TwoFactor.save(e);
e.valid = false; }));
return TwoFactor.save(e); twofactor = twofactor.filter(e => e.valid);
})); if (twofactor && twofactor.length > 0) {
twofactor = twofactor.filter(e => e.valid); let tfa = twofactor.map(e => {
if (twofactor && twofactor.length > 0) { return {
let tfa = twofactor.map(e => { id: e._id,
return { name: e.name || TFANames.get(e.type),
id: e._id, type: e.type
name: e.name, }
type: e.type })
} await sendToken(user, tfa);
}) } else {
await sendToken(user, tfa); await sendToken(user);
} else { }
await sendToken(user); }
} }
} } else {
} res.json({ error: req.__("Invalid type!") });
} else { }
res.json({ error: req.__("Invalid type!") }); });
}
});
export default Login; export default Login;

View File

@ -1,29 +1,29 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user"; import { GetUserMiddleware } from "../middlewares/user";
import LoginToken, { CheckToken } from "../../models/login_token"; import LoginToken, { CheckToken } from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
export const GetToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { export const GetToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
let raw_token = await LoginToken.find({ user: req.user._id, valid: true }); let raw_token = await LoginToken.find({ user: req.user._id, valid: true });
let token = await Promise.all(raw_token.map(async token => { let token = await Promise.all(raw_token.map(async token => {
await CheckToken(token); await CheckToken(token);
return { return {
id: token._id, id: token._id,
special: token.special, special: token.special,
ip: token.ip, ip: token.ip,
browser: token.browser, browser: token.browser,
isthis: token._id.equals(token.special ? req.token.special._id : req.token.login._id) isthis: token._id.equals(token.special ? req.token.special._id : req.token.login._id)
} }
}).filter(t => t !== undefined)); }).filter(t => t !== undefined));
res.json({ token }); res.json({ token });
}); });
export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => { export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
let { id } = req.query; let { id } = req.params;
let token = await LoginToken.findById(id); let token = await LoginToken.findById(id);
if (!token || !token.user.equals(req.user._id)) throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST); if (!token || !token.user.equals(req.user._id)) throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
token.valid = false; token.valid = false;
await LoginToken.save(token); await LoginToken.save(token);
res.json({ success: true }); res.json({ success: true });
}); });

View File

@ -0,0 +1,74 @@
import { Router } from "express"
import Stacker from "../../../middlewares/stacker";
import { GetUserMiddleware } from "../../../middlewares/user";
import TwoFactor, { TFATypes as TwoFATypes, IBackupCode } from "../../../../models/twofactor";
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment");
import { upgradeToken } from "../helper";
import * as crypto from "crypto";
import Logging from "@hibas123/nodelogging";
const BackupCodeRoute = Router();
// TODO: Further checks if this is good enough randomness
function generateCode(length: number) {
let bytes = crypto.randomBytes(length);
let nrs = "";
bytes.forEach((b, idx) => {
let nr = Math.floor((b / 255) * 9.9999)
if (nr > 9) nr = 9;
nrs += String(nr);
})
return nrs;
}
BackupCodeRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
//Generating new
let codes = Array(10).map(() => generateCode(8));
console.log(codes);
let twofactor = TwoFactor.new(<IBackupCode>{
user: req.user._id,
type: TwoFATypes.OTC,
valid: true,
data: codes,
name: ""
})
await TwoFactor.save(twofactor);
res.json({
codes,
id: twofactor._id
})
}));
BackupCodeRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
let { login, special } = req.token;
let { id, code }: { id: string, code: string } = req.body;
let twofactor: IBackupCode = await TwoFactor.findById(id);
if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false;
await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
code = code.replace(/\s/g, "");
let valid = twofactor.data.find(c => c === code);
if (valid) {
twofactor.data = twofactor.data.filter(c => c !== code);
await TwoFactor.save(twofactor);
let [login_exp, special_exp] = await Promise.all([
upgradeToken(login),
upgradeToken(special)
]);
res.json({ success: true, login_exp, special_exp })
} else {
throw new RequestError("Invalid or already used code!", HttpStatusCode.BAD_REQUEST);
}
}))
export default BackupCodeRoute;

View File

@ -1,79 +1,13 @@
import LoginToken, { ILoginToken } from "../../../models/login_token"; import LoginToken, { ILoginToken } from "../../../models/login_token";
import moment = require("moment"); import moment = require("moment");
// export async function unlockToken() { export async function upgradeToken(token: ILoginToken) {
// let { type, code, login, special } = req.body; token.data = undefined;
token.valid = true;
// let [login_t, special_t] = await Promise.all([LoginToken.findOne({ token: login }), LoginToken.findOne({ token: special })]); token.validated = true;
//TODO durations from config
// if ((login && !login_t) || (special && !special_t)) { let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate();
// res.json({ error: req.__("Token not found!") }); token.validTill = expires;
// } else { await LoginToken.save(token);
// let atoken = special_t || login_t; return expires;
// let user = await User.findById(atoken.user);
// let tf = await TwoFactor.find({ user: user._id, valid: true })
// let valid = false;
// switch (type) {
// case TokenTypes.OTC: {
// let twofactor = await TwoFactor.findOne({ type, valid: true })
// if (twofactor) {
// valid = speakeasy.totp.verify({
// secret: twofactor.token,
// encoding: "base64",
// token: code
// })
// }
// break;
// }
// case TokenTypes.BACKUP_CODE: {
// let twofactor = await TwoFactor.findOne({ type, valid: true, token: code })
// if (twofactor) {
// twofactor.valid = false;
// await TwoFactor.save(twofactor);
// valid = true;
// }
// break;
// }
// case TokenTypes.APP_ALLOW:
// case TokenTypes.YUBI_KEY:
// default:
// res.json({ error: req.__("Invalid twofactor!") });
// return;
// }
// if (!valid) {
// res.json({ error: req.__("Invalid code!") });
// return;
// }
// let result: any = {};
// if (login_t) {
// login_t.validated = true
// await LoginToken.save(login_t)
// result.login = { token: login_t.token, expires: login_t.validTill.toUTCString() }
// }
// if (special_t) {
// special_t.validated = true;
// await LoginToken.save(special_t);
// result.special = { token: special_t.token, expires: special_t.validTill.toUTCString() }
// }
// res.json(result);
// }
// }
export async function upgradeToken(token: ILoginToken) {
token.data = undefined;
token.valid = true;
token.validated = true;
//TODO durations from config
let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate();
token.validTill = expires;
await LoginToken.save(token);
return expires;
} }

View File

@ -1,42 +1,46 @@
import { Router } from "express"; import { Router } from "express";
import YubiKeyRoute from "./yubikey"; import YubiKeyRoute from "./yubikey";
import { GetUserMiddleware } from "../../middlewares/user"; import { GetUserMiddleware } from "../../middlewares/user";
import Stacker from "../../middlewares/stacker"; import Stacker from "../../middlewares/stacker";
import TwoFactor from "../../../models/twofactor"; import TwoFactor from "../../../models/twofactor";
import * as moment from "moment" import * as moment from "moment"
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import OTCRoute from "./otc";
const TwoFactorRouter = Router(); import BackupCodeRoute from "./backup";
TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { const TwoFactorRouter = Router();
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true })
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false) TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
await Promise.all(expired.map(e => { let twofactor = await TwoFactor.find({ user: req.user._id, valid: true })
e.valid = false; let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
return TwoFactor.save(e); await Promise.all(expired.map(e => {
})); e.valid = false;
twofactor = twofactor.filter(e => e.valid); return TwoFactor.save(e);
let tfa = twofactor.map(e => { }));
return { twofactor = twofactor.filter(e => e.valid);
id: e._id, let tfa = twofactor.map(e => {
name: e.name, return {
type: e.type id: e._id,
} name: e.name,
}) type: e.type
res.json({ methods: tfa }); }
})); })
res.json({ methods: tfa });
TwoFactorRouter.delete("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { }));
let { id } = req.query;
let tfa = await TwoFactor.findById(id); TwoFactorRouter.delete("/:id", Stacker(GetUserMiddleware(true, true), async (req, res) => {
if (!tfa || !tfa.user.equals(req.user._id)) { let { id } = req.params;
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST); let tfa = await TwoFactor.findById(id);
} if (!tfa || !tfa.user.equals(req.user._id)) {
tfa.valid = false; throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
await TwoFactor.save(tfa); }
res.json({ success: true }); tfa.valid = false;
})); await TwoFactor.save(tfa);
res.json({ success: true });
TwoFactorRouter.use("/yubikey", YubiKeyRoute); }));
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
TwoFactorRouter.use("/otc", OTCRoute);
TwoFactorRouter.use("/backup", BackupCodeRoute);
export default TwoFactorRouter; export default TwoFactorRouter;

View File

@ -0,0 +1,105 @@
import { Router } from "express"
import Stacker from "../../../middlewares/stacker";
import { GetUserMiddleware } from "../../../middlewares/user";
import TwoFactor, { TFATypes as TwoFATypes, IOTC } from "../../../../models/twofactor";
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment");
import { upgradeToken } from "../helper";
import Logging from "@hibas123/nodelogging";
import * as speakeasy from "speakeasy";
import * as qrcode from "qrcode";
import config from "../../../../config";
const OTCRoute = Router();
OTCRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
const { type } = req.query;
if (type === "create") {
//Generating new
let secret = speakeasy.generateSecret({
name: config.core.name,
issuer: config.core.name
});
let twofactor = TwoFactor.new(<IOTC>{
user: req.user._id,
type: TwoFATypes.OTC,
valid: false,
data: secret.base32
})
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
await TwoFactor.save(twofactor);
res.json({
image: dataurl,
id: twofactor._id
})
} else if (type === "validate") {
// Checking code and marking as valid
const { code, id } = req.body;
Logging.debug(req.body, id);
let twofactor: IOTC = await TwoFactor.findById(id);
const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) };
if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC || !twofactor.data || twofactor.valid) {
Logging.debug("Not found or wrong user", twofactor);
err();
}
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
await TwoFactor.delete(twofactor);
Logging.debug("Expired!", twofactor);
err();
}
let valid = speakeasy.totp.verify({
secret: twofactor.data,
encoding: "base32",
token: code
})
if (valid) {
twofactor.expires = undefined;
twofactor.valid = true;
await TwoFactor.save(twofactor);
res.json({ success: true });
} else {
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
}
} else {
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
}
}));
OTCRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
let { login, special } = req.token;
let { id, code } = req.body;
let twofactor: IOTC = await TwoFactor.findById(id);
if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
twofactor.valid = false;
await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
}
let valid = speakeasy.totp.verify({
secret: twofactor.data,
encoding: "base32",
token: code
})
if (valid) {
let [login_exp, special_exp] = await Promise.all([
upgradeToken(login),
upgradeToken(special)
]);
res.json({ success: true, login_exp, special_exp })
} else {
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
}
}))
export default OTCRoute;

View File

@ -1,131 +1,134 @@
import { Router, Request } from "express" import { Router, Request } from "express"
import Stacker from "../../../middlewares/stacker"; import Stacker from "../../../middlewares/stacker";
import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user"; import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user";
import * as u2f from "u2f"; import * as u2f from "u2f";
import config from "../../../../config"; import config from "../../../../config";
import TwoFactor, { TFATypes as TwoFATypes, IYubiKey } from "../../../../models/twofactor"; import TwoFactor, { TFATypes as TwoFATypes, IYubiKey } from "../../../../models/twofactor";
import RequestError, { HttpStatusCode } from "../../../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
import moment = require("moment"); import moment = require("moment");
import LoginToken from "../../../../models/login_token"; import LoginToken from "../../../../models/login_token";
import { upgradeToken } from "../helper"; import { upgradeToken } from "../helper";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
const U2FRoute = Router(); const U2FRoute = Router();
U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => { /**
const { type } = req.query; * Registerinf a new YubiKey
if (type === "challenge") { */
const registrationRequest = u2f.request(config.core.url); U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
const { type } = req.query;
let twofactor = TwoFactor.new(<IYubiKey>{ if (type === "challenge") {
user: req.user._id, const registrationRequest = u2f.request(config.core.url);
type: TwoFATypes.U2F,
valid: false, let twofactor = TwoFactor.new(<IYubiKey>{
data: { user: req.user._id,
registration: registrationRequest type: TwoFATypes.U2F,
} valid: false,
}) data: {
await TwoFactor.save(twofactor); registration: registrationRequest
res.json({ request: registrationRequest, id: twofactor._id, appid: config.core.url }); }
} else { })
const { response, id } = req.body; await TwoFactor.save(twofactor);
Logging.debug(req.body, id); res.json({ request: registrationRequest, id: twofactor._id, appid: config.core.url });
let twofactor: IYubiKey = await TwoFactor.findById(id); } else {
const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) }; const { response, id } = req.body;
if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.U2F || !twofactor.data.registration || twofactor.valid) { Logging.debug(req.body, id);
Logging.debug("Not found or wrong user", twofactor); let twofactor: IYubiKey = await TwoFactor.findById(id);
err(); const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) };
} if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.U2F || !twofactor.data.registration || twofactor.valid) {
Logging.debug("Not found or wrong user", twofactor);
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) { err();
await TwoFactor.delete(twofactor); }
Logging.debug("Expired!", twofactor);
err(); if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
} await TwoFactor.delete(twofactor);
Logging.debug("Expired!", twofactor);
const result = u2f.checkRegistration(twofactor.data.registration, response); err();
}
if (result.successful) {
twofactor.data = { const result = u2f.checkRegistration(twofactor.data.registration, response);
keyHandle: result.keyHandle,
publicKey: result.publicKey if (result.successful) {
} twofactor.data = {
twofactor.expires = undefined; keyHandle: result.keyHandle,
twofactor.valid = true; publicKey: result.publicKey
await TwoFactor.save(twofactor); }
res.json({ success: true }); twofactor.expires = undefined;
} else { twofactor.valid = true;
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); await TwoFactor.save(twofactor);
} res.json({ success: true });
} } else {
})); throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
}
U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { }
let { login, special } = req.token; }));
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
if (!twofactor) { let { login, special } = req.token;
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
}
if (!twofactor) {
if (twofactor.expires) { throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
if (moment().isAfter(twofactor.expires)) { }
twofactor.valid = false;
await TwoFactor.save(twofactor); if (twofactor.expires) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); if (moment().isAfter(twofactor.expires)) {
} twofactor.valid = false;
} await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
let request = u2f.request(config.core.url, twofactor.data.keyHandle); }
login.data = { }
type: "ykr",
request let request = u2f.request(config.core.url, twofactor.data.keyHandle);
}; login.data = {
let r;; type: "ykr",
if (special) { request
special.data = login.data; };
r = LoginToken.save(special); let r;;
} if (special) {
special.data = login.data;
await Promise.all([r, LoginToken.save(login)]); r = LoginToken.save(special);
res.json({ request }); }
}))
await Promise.all([r, LoginToken.save(login)]);
U2FRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => { res.json({ request });
let { login, special } = req.token; }))
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
U2FRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
let { login, special } = req.token;
let { response } = req.body; let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
if (!twofactor || !login.data || login.data.type !== "ykr" || special && (!special.data || special.data.type !== "ykr")) {
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
} let { response } = req.body;
if (!twofactor || !login.data || login.data.type !== "ykr" || special && (!special.data || special.data.type !== "ykr")) {
if (twofactor.expires && moment().isAfter(twofactor.expires)) { throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
twofactor.valid = false; }
await TwoFactor.save(twofactor);
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST); if (twofactor.expires && moment().isAfter(twofactor.expires)) {
} twofactor.valid = false;
await TwoFactor.save(twofactor);
let login_exp; throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
let special_exp; }
let result = u2f.checkSignature(login.data.request, response, twofactor.data.publicKey);
if (result.successful) { let login_exp;
if (special) { let special_exp;
let result = u2f.checkSignature(special.data.request, response, twofactor.data.publicKey); let result = u2f.checkSignature(login.data.request, response, twofactor.data.publicKey);
if (result.successful) { if (result.successful) {
special_exp = await upgradeToken(special); if (special) {
} let result = u2f.checkSignature(special.data.request, response, twofactor.data.publicKey);
else { if (result.successful) {
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); special_exp = await upgradeToken(special);
} }
} else {
login_exp = await upgradeToken(login); throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
} }
else { }
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST); login_exp = await upgradeToken(login);
} }
res.json({ success: true, login_exp, special_exp }) else {
})) throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
}
export default U2FRoute; res.json({ success: true, login_exp, special_exp })
}))
export default U2FRoute;

View File

@ -1,388 +1,388 @@
/** /**
* Hypertext Transfer Protocol (HTTP) response status codes. * Hypertext Transfer Protocol (HTTP) response status codes.
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
*/ */
export enum HttpStatusCode { export enum HttpStatusCode {
/** /**
* The server has received the request headers and the client should proceed to send the request body * The server has received the request headers and the client should proceed to send the request body
* (in the case of a request for which a body needs to be sent; for example, a POST request). * (in the case of a request for which a body needs to be sent; for example, a POST request).
* Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient.
* To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request
* and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued.
*/ */
CONTINUE = 100, CONTINUE = 100,
/** /**
* The requester has asked the server to switch protocols and the server has agreed to do so. * The requester has asked the server to switch protocols and the server has agreed to do so.
*/ */
SWITCHING_PROTOCOLS = 101, SWITCHING_PROTOCOLS = 101,
/** /**
* A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request.
* This code indicates that the server has received and is processing the request, but no response is available yet. * This code indicates that the server has received and is processing the request, but no response is available yet.
* This prevents the client from timing out and assuming the request was lost. * This prevents the client from timing out and assuming the request was lost.
*/ */
PROCESSING = 102, PROCESSING = 102,
/** /**
* Standard response for successful HTTP requests. * Standard response for successful HTTP requests.
* The actual response will depend on the request method used. * The actual response will depend on the request method used.
* In a GET request, the response will contain an entity corresponding to the requested resource. * In a GET request, the response will contain an entity corresponding to the requested resource.
* In a POST request, the response will contain an entity describing or containing the result of the action. * In a POST request, the response will contain an entity describing or containing the result of the action.
*/ */
OK = 200, OK = 200,
/** /**
* The request has been fulfilled, resulting in the creation of a new resource. * The request has been fulfilled, resulting in the creation of a new resource.
*/ */
CREATED = 201, CREATED = 201,
/** /**
* The request has been accepted for processing, but the processing has not been completed. * The request has been accepted for processing, but the processing has not been completed.
* The request might or might not be eventually acted upon, and may be disallowed when processing occurs. * The request might or might not be eventually acted upon, and may be disallowed when processing occurs.
*/ */
ACCEPTED = 202, ACCEPTED = 202,
/** /**
* SINCE HTTP/1.1 * SINCE HTTP/1.1
* The server is a transforming proxy that received a 200 OK from its origin, * The server is a transforming proxy that received a 200 OK from its origin,
* but is returning a modified version of the origin's response. * but is returning a modified version of the origin's response.
*/ */
NON_AUTHORITATIVE_INFORMATION = 203, NON_AUTHORITATIVE_INFORMATION = 203,
/** /**
* The server successfully processed the request and is not returning any content. * The server successfully processed the request and is not returning any content.
*/ */
NO_CONTENT = 204, NO_CONTENT = 204,
/** /**
* The server successfully processed the request, but is not returning any content. * The server successfully processed the request, but is not returning any content.
* Unlike a 204 response, this response requires that the requester reset the document view. * Unlike a 204 response, this response requires that the requester reset the document view.
*/ */
RESET_CONTENT = 205, RESET_CONTENT = 205,
/** /**
* The server is delivering only part of the resource (byte serving) due to a range header sent by the client. * The server is delivering only part of the resource (byte serving) due to a range header sent by the client.
* The range header is used by HTTP clients to enable resuming of interrupted downloads, * The range header is used by HTTP clients to enable resuming of interrupted downloads,
* or split a download into multiple simultaneous streams. * or split a download into multiple simultaneous streams.
*/ */
PARTIAL_CONTENT = 206, PARTIAL_CONTENT = 206,
/** /**
* The message body that follows is an XML message and can contain a number of separate response codes, * The message body that follows is an XML message and can contain a number of separate response codes,
* depending on how many sub-requests were made. * depending on how many sub-requests were made.
*/ */
MULTI_STATUS = 207, MULTI_STATUS = 207,
/** /**
* The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response,
* and are not being included again. * and are not being included again.
*/ */
ALREADY_REPORTED = 208, ALREADY_REPORTED = 208,
/** /**
* The server has fulfilled a request for the resource, * The server has fulfilled a request for the resource,
* and the response is a representation of the result of one or more instance-manipulations applied to the current instance. * and the response is a representation of the result of one or more instance-manipulations applied to the current instance.
*/ */
IM_USED = 226, IM_USED = 226,
/** /**
* Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation).
* For example, this code could be used to present multiple video format options, * For example, this code could be used to present multiple video format options,
* to list files with different filename extensions, or to suggest word-sense disambiguation. * to list files with different filename extensions, or to suggest word-sense disambiguation.
*/ */
MULTIPLE_CHOICES = 300, MULTIPLE_CHOICES = 300,
/** /**
* This and all future requests should be directed to the given URI. * This and all future requests should be directed to the given URI.
*/ */
MOVED_PERMANENTLY = 301, MOVED_PERMANENTLY = 301,
/** /**
* This is an example of industry practice contradicting the standard. * This is an example of industry practice contradicting the standard.
* The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect
* (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302
* with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307
* to distinguish between the two behaviours. However, some Web applications and frameworks * to distinguish between the two behaviours. However, some Web applications and frameworks
* use the 302 status code as if it were the 303. * use the 302 status code as if it were the 303.
*/ */
FOUND = 302, FOUND = 302,
/** /**
* SINCE HTTP/1.1 * SINCE HTTP/1.1
* The response to the request can be found under another URI using a GET method. * The response to the request can be found under another URI using a GET method.
* When received in response to a POST (or PUT/DELETE), the client should presume that * When received in response to a POST (or PUT/DELETE), the client should presume that
* the server has received the data and should issue a redirect with a separate GET message. * the server has received the data and should issue a redirect with a separate GET message.
*/ */
SEE_OTHER = 303, SEE_OTHER = 303,
/** /**
* Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match.
* In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy.
*/ */
NOT_MODIFIED = 304, NOT_MODIFIED = 304,
/** /**
* SINCE HTTP/1.1 * SINCE HTTP/1.1
* The requested resource is available only through a proxy, the address for which is provided in the response. * The requested resource is available only through a proxy, the address for which is provided in the response.
* Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons.
*/ */
USE_PROXY = 305, USE_PROXY = 305,
/** /**
* No longer used. Originally meant "Subsequent requests should use the specified proxy." * No longer used. Originally meant "Subsequent requests should use the specified proxy."
*/ */
SWITCH_PROXY = 306, SWITCH_PROXY = 306,
/** /**
* SINCE HTTP/1.1 * SINCE HTTP/1.1
* In this case, the request should be repeated with another URI; however, future requests should still use the original URI. * In this case, the request should be repeated with another URI; however, future requests should still use the original URI.
* In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request.
* For example, a POST request should be repeated using another POST request. * For example, a POST request should be repeated using another POST request.
*/ */
TEMPORARY_REDIRECT = 307, TEMPORARY_REDIRECT = 307,
/** /**
* The request and all future requests should be repeated using another URI. * The request and all future requests should be repeated using another URI.
* 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change.
* So, for example, submitting a form to a permanently redirected resource may continue smoothly. * So, for example, submitting a form to a permanently redirected resource may continue smoothly.
*/ */
PERMANENT_REDIRECT = 308, PERMANENT_REDIRECT = 308,
/** /**
* The server cannot or will not process the request due to an apparent client error * The server cannot or will not process the request due to an apparent client error
* (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing).
*/ */
BAD_REQUEST = 400, BAD_REQUEST = 400,
/** /**
* Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet
* been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the
* requested resource. See Basic access authentication and Digest access authentication. 401 semantically means * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means
* "unauthenticated",i.e. the user does not have the necessary credentials. * "unauthenticated",i.e. the user does not have the necessary credentials.
*/ */
UNAUTHORIZED = 401, UNAUTHORIZED = 401,
/** /**
* Reserved for future use. The original intention was that this code might be used as part of some form of digital * Reserved for future use. The original intention was that this code might be used as part of some form of digital
* cash or micro payment scheme, but that has not happened, and this code is not usually used. * cash or micro payment scheme, but that has not happened, and this code is not usually used.
* Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests.
*/ */
PAYMENT_REQUIRED = 402, PAYMENT_REQUIRED = 402,
/** /**
* The request was valid, but the server is refusing action. * The request was valid, but the server is refusing action.
* The user might not have the necessary permissions for a resource. * The user might not have the necessary permissions for a resource.
*/ */
FORBIDDEN = 403, FORBIDDEN = 403,
/** /**
* The requested resource could not be found but may be available in the future. * The requested resource could not be found but may be available in the future.
* Subsequent requests by the client are permissible. * Subsequent requests by the client are permissible.
*/ */
NOT_FOUND = 404, NOT_FOUND = 404,
/** /**
* A request method is not supported for the requested resource; * A request method is not supported for the requested resource;
* for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource.
*/ */
METHOD_NOT_ALLOWED = 405, METHOD_NOT_ALLOWED = 405,
/** /**
* The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request.
*/ */
NOT_ACCEPTABLE = 406, NOT_ACCEPTABLE = 406,
/** /**
* The client must first authenticate itself with the proxy. * The client must first authenticate itself with the proxy.
*/ */
PROXY_AUTHENTICATION_REQUIRED = 407, PROXY_AUTHENTICATION_REQUIRED = 407,
/** /**
* The server timed out waiting for the request. * The server timed out waiting for the request.
* According to HTTP specifications: * According to HTTP specifications:
* "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time."
*/ */
REQUEST_TIMEOUT = 408, REQUEST_TIMEOUT = 408,
/** /**
* Indicates that the request could not be processed because of conflict in the request, * Indicates that the request could not be processed because of conflict in the request,
* such as an edit conflict between multiple simultaneous updates. * such as an edit conflict between multiple simultaneous updates.
*/ */
CONFLICT = 409, CONFLICT = 409,
/** /**
* Indicates that the resource requested is no longer available and will not be available again. * Indicates that the resource requested is no longer available and will not be available again.
* This should be used when a resource has been intentionally removed and the resource should be purged. * This should be used when a resource has been intentionally removed and the resource should be purged.
* Upon receiving a 410 status code, the client should not request the resource in the future. * Upon receiving a 410 status code, the client should not request the resource in the future.
* Clients such as search engines should remove the resource from their indices. * Clients such as search engines should remove the resource from their indices.
* Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead.
*/ */
GONE = 410, GONE = 410,
/** /**
* The request did not specify the length of its content, which is required by the requested resource. * The request did not specify the length of its content, which is required by the requested resource.
*/ */
LENGTH_REQUIRED = 411, LENGTH_REQUIRED = 411,
/** /**
* The server does not meet one of the preconditions that the requester put on the request. * The server does not meet one of the preconditions that the requester put on the request.
*/ */
PRECONDITION_FAILED = 412, PRECONDITION_FAILED = 412,
/** /**
* The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large".
*/ */
PAYLOAD_TOO_LARGE = 413, PAYLOAD_TOO_LARGE = 413,
/** /**
* The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request,
* in which case it should be converted to a POST request. * in which case it should be converted to a POST request.
* Called "Request-URI Too Long" previously. * Called "Request-URI Too Long" previously.
*/ */
URI_TOO_LONG = 414, URI_TOO_LONG = 414,
/** /**
* The request entity has a media type which the server or resource does not support. * The request entity has a media type which the server or resource does not support.
* For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format.
*/ */
UNSUPPORTED_MEDIA_TYPE = 415, UNSUPPORTED_MEDIA_TYPE = 415,
/** /**
* The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion.
* For example, if the client asked for a part of the file that lies beyond the end of the file. * For example, if the client asked for a part of the file that lies beyond the end of the file.
* Called "Requested Range Not Satisfiable" previously. * Called "Requested Range Not Satisfiable" previously.
*/ */
RANGE_NOT_SATISFIABLE = 416, RANGE_NOT_SATISFIABLE = 416,
/** /**
* The server cannot meet the requirements of the Expect request-header field. * The server cannot meet the requirements of the Expect request-header field.
*/ */
EXPECTATION_FAILED = 417, EXPECTATION_FAILED = 417,
/** /**
* This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol,
* and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by
* teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com.
*/ */
I_AM_A_TEAPOT = 418, I_AM_A_TEAPOT = 418,
/** /**
* The request was directed at a server that is not able to produce a response (for example because a connection reuse). * The request was directed at a server that is not able to produce a response (for example because a connection reuse).
*/ */
MISDIRECTED_REQUEST = 421, MISDIRECTED_REQUEST = 421,
/** /**
* The request was well-formed but was unable to be followed due to semantic errors. * The request was well-formed but was unable to be followed due to semantic errors.
*/ */
UNPROCESSABLE_ENTITY = 422, UNPROCESSABLE_ENTITY = 422,
/** /**
* The resource that is being accessed is locked. * The resource that is being accessed is locked.
*/ */
LOCKED = 423, LOCKED = 423,
/** /**
* The request failed due to failure of a previous request (e.g., a PROPPATCH). * The request failed due to failure of a previous request (e.g., a PROPPATCH).
*/ */
FAILED_DEPENDENCY = 424, FAILED_DEPENDENCY = 424,
/** /**
* The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field.
*/ */
UPGRADE_REQUIRED = 426, UPGRADE_REQUIRED = 426,
/** /**
* The origin server requires the request to be conditional. * The origin server requires the request to be conditional.
* Intended to prevent "the 'lost update' problem, where a client * Intended to prevent "the 'lost update' problem, where a client
* GETs a resource's state, modifies it, and PUTs it back to the server, * GETs a resource's state, modifies it, and PUTs it back to the server,
* when meanwhile a third party has modified the state on the server, leading to a conflict." * when meanwhile a third party has modified the state on the server, leading to a conflict."
*/ */
PRECONDITION_REQUIRED = 428, PRECONDITION_REQUIRED = 428,
/** /**
* The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes.
*/ */
TOO_MANY_REQUESTS = 429, TOO_MANY_REQUESTS = 429,
/** /**
* The server is unwilling to process the request because either an individual header field, * The server is unwilling to process the request because either an individual header field,
* or all the header fields collectively, are too large. * or all the header fields collectively, are too large.
*/ */
REQUEST_HEADER_FIELDS_TOO_LARGE = 431, REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
/** /**
* A server operator has received a legal demand to deny access to a resource or to a set of resources * A server operator has received a legal demand to deny access to a resource or to a set of resources
* that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451.
*/ */
UNAVAILABLE_FOR_LEGAL_REASONS = 451, UNAVAILABLE_FOR_LEGAL_REASONS = 451,
/** /**
* A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.
*/ */
INTERNAL_SERVER_ERROR = 500, INTERNAL_SERVER_ERROR = 500,
/** /**
* The server either does not recognize the request method, or it lacks the ability to fulfill the request. * The server either does not recognize the request method, or it lacks the ability to fulfill the request.
* Usually this implies future availability (e.g., a new feature of a web-service API). * Usually this implies future availability (e.g., a new feature of a web-service API).
*/ */
NOT_IMPLEMENTED = 501, NOT_IMPLEMENTED = 501,
/** /**
* The server was acting as a gateway or proxy and received an invalid response from the upstream server. * The server was acting as a gateway or proxy and received an invalid response from the upstream server.
*/ */
BAD_GATEWAY = 502, BAD_GATEWAY = 502,
/** /**
* The server is currently unavailable (because it is overloaded or down for maintenance). * The server is currently unavailable (because it is overloaded or down for maintenance).
* Generally, this is a temporary state. * Generally, this is a temporary state.
*/ */
SERVICE_UNAVAILABLE = 503, SERVICE_UNAVAILABLE = 503,
/** /**
* The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.
*/ */
GATEWAY_TIMEOUT = 504, GATEWAY_TIMEOUT = 504,
/** /**
* The server does not support the HTTP protocol version used in the request * The server does not support the HTTP protocol version used in the request
*/ */
HTTP_VERSION_NOT_SUPPORTED = 505, HTTP_VERSION_NOT_SUPPORTED = 505,
/** /**
* Transparent content negotiation for the request results in a circular reference. * Transparent content negotiation for the request results in a circular reference.
*/ */
VARIANT_ALSO_NEGOTIATES = 506, VARIANT_ALSO_NEGOTIATES = 506,
/** /**
* The server is unable to store the representation needed to complete the request. * The server is unable to store the representation needed to complete the request.
*/ */
INSUFFICIENT_STORAGE = 507, INSUFFICIENT_STORAGE = 507,
/** /**
* The server detected an infinite loop while processing the request. * The server detected an infinite loop while processing the request.
*/ */
LOOP_DETECTED = 508, LOOP_DETECTED = 508,
/** /**
* Further extensions to the request are required for the server to fulfill it. * Further extensions to the request are required for the server to fulfill it.
*/ */
NOT_EXTENDED = 510, NOT_EXTENDED = 510,
/** /**
* The client needs to authenticate to gain network access. * The client needs to authenticate to gain network access.
* Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used
* to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot).
*/ */
NETWORK_AUTHENTICATION_REQUIRED = 511 NETWORK_AUTHENTICATION_REQUIRED = 511
} }
export default class RequestError extends Error { export default class RequestError extends Error {
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false) { constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false, public additional: any = undefined) {
super("") super("")
this.message = message; this.message = message;
} }
} }

View File

@ -1,73 +1,79 @@
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import config from "./config"; import config from "./config";
import NLS from "@hibas123/nodeloggingserver_client"; import NLS from "@hibas123/nodeloggingserver_client";
if (config.logging) { if (config.logging) {
let s = NLS(Logging, config.logging.server, config.logging.appid, config.logging.token); let s = NLS(Logging, config.logging.server, config.logging.appid, config.logging.token);
s.send(`[${new Date().toLocaleTimeString()}] Starting application`); s.send(`[${new Date().toLocaleTimeString()}] Starting application`);
} }
// if (!config.database) { // if (!config.database) {
// Logging.error("No database config set. Terminating.") // Logging.error("No database config set. Terminating.")
// process.exit(); // process.exit();
// } // }
if (!config.web) { if (!config.web) {
Logging.error("No web config set. Terminating.") Logging.error("No web config set. Terminating.")
process.exit(); process.exit();
} }
import * as i18n from "i18n" import * as i18n from "i18n"
i18n.configure({ i18n.configure({
locales: ["en", "de"], locales: ["en", "de"],
directory: "./locales", directory: "./locales",
}) })
import Web from "./web"; import Web from "./web";
import TestData from "./testdata"; import TestData from "./testdata";
import DB from "./database"; import DB from "./database";
Logging.log("Connecting to Database") Logging.log("Connecting to Database")
if (config.dev) { if (config.dev) {
Logging.warning("Running in dev mode! Database will be cleared!") Logging.warning("Running in dev mode! Database will be cleared!")
} }
DB.connect().then(async () => { DB.connect().then(async () => {
Logging.log("Database connected") Logging.log("Database connected")
if (config.dev) if (config.dev)
await TestData() await TestData()
let web = new Web(config.web) let web = new Web(config.web)
web.listen() web.listen()
function print(path, layer) {
if (layer.route) { let already = new Set();
layer.route.stack.forEach(print.bind(null, path.concat(split(layer.route.path)))) function print(path, layer) {
} else if (layer.name === 'router' && layer.handle.stack) { if (layer.route) {
layer.handle.stack.forEach(print.bind(null, path.concat(split(layer.regexp)))) layer.route.stack.forEach(print.bind(null, path.concat(split(layer.route.path))))
} else if (layer.method) { } else if (layer.name === 'router' && layer.handle.stack) {
let me: string = layer.method.toUpperCase(); layer.handle.stack.forEach(print.bind(null, path.concat(split(layer.regexp))))
me += " ".repeat(6 - me.length); } else if (layer.method) {
Logging.log(`${me} /${path.concat(split(layer.regexp)).filter(Boolean).join('/')}`); let me: string = layer.method.toUpperCase();
} me += " ".repeat(6 - me.length);
} let msg = `${me} /${path.concat(split(layer.regexp)).filter(Boolean).join('/')}`;
if (!already.has(msg)) {
function split(thing) { already.add(msg);
if (typeof thing === 'string') { Logging.log(msg);
return thing.split('/') }
} else if (thing.fast_slash) { }
return '' }
} else {
var match = thing.toString() function split(thing) {
.replace('\\/?', '') if (typeof thing === 'string') {
.replace('(?=\\/|$)', '$') return thing.split('/')
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//) } else if (thing.fast_slash) {
return match return ''
? match[1].replace(/\\(.)/g, '$1').split('/') } else {
: '<complex:' + thing.toString() + '>' var match = thing.toString()
} .replace('\\/?', '')
} .replace('(?=\\/|$)', '$')
Logging.log("--- Endpoints: ---"); .match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//)
web.server._router.stack.forEach(print.bind(null, [])) return match
Logging.log("--- Endpoints end ---") ? match[1].replace(/\\(.)/g, '$1').split('/')
}).catch(e => { : '<complex:' + thing.toString() + '>'
Logging.error(e) }
process.exit(); }
// Logging.log("--- Endpoints: ---");
// web.server._router.stack.forEach(print.bind(null, []))
// Logging.log("--- Endpoints end ---")
}).catch(e => {
Logging.error(e)
process.exit();
}) })

View File

@ -1,65 +1,67 @@
import DB from "../database"; import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model"; import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb"; import { ObjectID } from "mongodb";
import moment = require("moment"); import moment = require("moment");
export interface ILoginToken extends ModelDataBase { export interface ILoginToken extends ModelDataBase {
token: string; token: string;
special: boolean; special: boolean;
user: ObjectID; user: ObjectID;
validTill: Date; validTill: Date;
valid: boolean; valid: boolean;
validated: boolean; validated: boolean;
data: any; data: any;
ip: string; ip: string;
browser: string; browser: string;
} }
const LoginToken = DB.addModel<ILoginToken>({ const LoginToken = DB.addModel<ILoginToken>({
name: "login_token", name: "login_token",
versions: [{ versions: [{
migration: () => { }, migration: () => { },
schema: { schema: {
token: { type: String }, token: { type: String },
special: { type: Boolean, default: () => false }, special: { type: Boolean, default: () => false },
user: { type: ObjectID }, user: { type: ObjectID },
validTill: { type: Date }, validTill: { type: Date },
valid: { type: Boolean } valid: { type: Boolean }
} }
}, { }, {
migration: (doc: ILoginToken) => { doc.validated = true; }, migration: (doc: ILoginToken) => { doc.validated = true; },
schema: { schema: {
token: { type: String }, token: { type: String },
special: { type: Boolean, default: () => false }, special: { type: Boolean, default: () => false },
user: { type: ObjectID }, user: { type: ObjectID },
validTill: { type: Date }, validTill: { type: Date },
valid: { type: Boolean }, valid: { type: Boolean },
validated: { type: Boolean, default: false } validated: { type: Boolean, default: false }
} }
}, { }, {
migration: (doc: ILoginToken) => { doc.validated = true; }, migration: (doc: ILoginToken) => { doc.validated = true; },
schema: { schema: {
token: { type: String }, token: { type: String },
special: { type: Boolean, default: () => false }, special: { type: Boolean, default: () => false },
user: { type: ObjectID }, user: { type: ObjectID },
validTill: { type: Date }, validTill: { type: Date },
valid: { type: Boolean }, valid: { type: Boolean },
validated: { type: Boolean, default: false }, validated: { type: Boolean, default: false },
data: { type: "any", optional: true }, data: { type: "any", optional: true },
ip: { type: String, optional: true }, ip: { type: String, optional: true },
browser: { type: String, optional: true } browser: { type: String, optional: true }
} }
}] }]
}) })
export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise<boolean> { export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise<boolean> {
if (!token || !token.valid) return false; if (!token || !token.valid)
if (validated && !token.validated) return false; return false;
if (moment().isAfter(token.validTill)) { if (validated && !token.validated)
token.valid = false; return false;
await LoginToken.save(token) if (moment().isAfter(token.validTill)) {
return false; token.valid = false;
} await LoginToken.save(token)
return true; return false;
} }
return true;
}
export default LoginToken; export default LoginToken;

View File

@ -1,57 +1,67 @@
import DB from "../database"; import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model"; import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "bson"; import { ObjectID } from "bson";
export enum TFATypes { export enum TFATypes {
OTC, OTC,
BACKUP_CODE, BACKUP_CODE,
U2F, U2F,
APP_ALLOW APP_ALLOW
} }
export interface ITwoFactor extends ModelDataBase { export const TFANames = new Map<TFATypes, string>();
user: ObjectID TFANames.set(TFATypes.OTC, "Authenticator");
valid: boolean TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
expires?: Date; TFANames.set(TFATypes.U2F, "Security Key (U2F)");
name?: string; TFANames.set(TFATypes.APP_ALLOW, "App Push");
type: TFATypes
data: any; export interface ITwoFactor extends ModelDataBase {
} user: ObjectID
valid: boolean
export interface IOTP extends ITwoFactor { expires?: Date;
data: string; name?: string;
} type: TFATypes
data: any;
export interface IYubiKey extends ITwoFactor { }
data: {
registration?: any; export interface IOTC extends ITwoFactor {
publicKey: string; data: string;
keyHandle: string; }
}
} export interface IYubiKey extends ITwoFactor {
data: {
export interface IU2F extends ITwoFactor { registration?: any;
data: { publicKey: string;
challenge?: string; keyHandle: string;
publicKey: string; }
keyHandle: string; }
registration?: string;
} export interface IU2F extends ITwoFactor {
} data: {
challenge?: string;
const TwoFactor = DB.addModel<ITwoFactor>({ publicKey: string;
name: "twofactor", keyHandle: string;
versions: [{ registration?: string;
migration: (e) => { }, }
schema: { }
user: { type: ObjectID },
valid: { type: Boolean }, export interface IBackupCode extends ITwoFactor {
expires: { type: Date, optional: true }, data: string[];
name: { type: String, optional: true }, }
type: { type: Number },
data: { type: "any" }, const TwoFactor = DB.addModel<ITwoFactor>({
} name: "twofactor",
}] versions: [{
}); migration: (e) => { },
schema: {
user: { type: ObjectID },
valid: { type: Boolean },
expires: { type: Date, optional: true },
name: { type: String, optional: true },
type: { type: Number },
data: { type: "any" },
}
}]
});
export default TwoFactor; export default TwoFactor;

View File

@ -1,80 +1,123 @@
import User, { Gender } from "./models/user"; import User, { Gender } from "./models/user";
import Client from "./models/client"; import Client from "./models/client";
import { Logging } from "@hibas123/nodelogging"; import { Logging } from "@hibas123/nodelogging";
import RegCode from "./models/regcodes"; import RegCode from "./models/regcodes";
import * as moment from "moment"; import * as moment from "moment";
import Permission from "./models/permissions"; import Permission from "./models/permissions";
import { ObjectID } from "bson"; import { ObjectID } from "bson";
import DB from "./database"; import DB from "./database";
import TwoFactor from "./models/twofactor"; import TwoFactor from "./models/twofactor";
export default async function TestData() {
await DB.db.dropDatabase(); import * as speakeasy from "speakeasy";
let u = await User.findOne({ username: "test" }); import LoginToken from "./models/login_token";
if (!u) { import { log } from "handlebars";
Logging.log("Adding test user");
u = User.new({ export default async function TestData() {
username: "test", await DB.db.dropDatabase();
birthday: new Date(), let u = await User.findOne({ username: "test" });
gender: Gender.male, if (!u) {
name: "Test Test", Logging.log("Adding test user");
password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc", u = User.new({
salt: "test", username: "test",
admin: true birthday: new Date(),
}) gender: Gender.male,
await User.save(u); name: "Test Test",
} password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
salt: "test",
let c = await Client.findOne({ client_id: "test001" }); admin: true
if (!c) { })
Logging.log("Adding test client") await User.save(u);
c = Client.new({ }
client_id: "test001",
client_secret: "test001", let c = await Client.findOne({ client_id: "test001" });
internal: true, if (!c) {
maintainer: u._id, Logging.log("Adding test client")
name: "Test Client", c = Client.new({
website: "http://example.com", client_id: "test001",
redirect_url: "http://example.com" client_secret: "test001",
}) internal: true,
await Client.save(c); maintainer: u._id,
} name: "Test Client",
website: "http://example.com",
let perm = await Permission.findOne({ id: 0 }); redirect_url: "http://example.com"
if (!perm) { })
Logging.log("Adding test permission") await Client.save(c);
perm = Permission.new({ }
_id: new ObjectID("507f1f77bcf86cd799439011"),
name: "TestPerm", let perm = await Permission.findOne({ id: 0 });
description: "Permission just for testing purposes", if (!perm) {
client: c._id Logging.log("Adding test permission")
}) perm = Permission.new({
Permission.save(perm); _id: new ObjectID("507f1f77bcf86cd799439011"),
} name: "TestPerm",
description: "Permission just for testing purposes",
let r = await RegCode.findOne({ token: "test" }); client: c._id
if (!r) { })
Logging.log("Adding test reg_code") Permission.save(perm);
r = RegCode.new({ }
token: "test",
valid: true, let r = await RegCode.findOne({ token: "test" });
validTill: moment().add("1", "year").toDate() if (!r) {
}) Logging.log("Adding test reg_code")
await RegCode.save(r); r = RegCode.new({
} token: "test",
valid: true,
let t = await TwoFactor.findOne({ user: u._id, type: 2 }) validTill: moment().add("1", "year").toDate()
if (!t) { })
t = TwoFactor.new({ await RegCode.save(r);
user: u._id, }
type: 2,
valid: true, let t = await TwoFactor.findOne({ user: u._id, type: 0 })
data: { if (!t) {
keyHandle: "tWSaMoHX2E96CoZOKOi_4aj6WVEh1e46FKXN0oDY2Z-laNOFcATlStNDo52HX7ygupW-v9qZOCX3J4d5nhOzWQ", t = TwoFactor.new({
publicKey: "BPsgBxR8M7MyrknlFuvYZv0Z1lZxiJQJNrLDA1yi3XKD_lrhIpnAh2OY_TsFjASvn3JTtwlCh62QdMvN-ejQL78" user: u._id,
}, type: 0,
expires: null valid: true,
}) data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
TwoFactor.save(t); expires: null
} })
TwoFactor.save(t);
}
let login_token = await LoginToken.findOne({ token: "test01" });
if (login_token)
await LoginToken.delete(login_token);
login_token = LoginToken.new({
browser: "DEMO",
ip: "10.0.0.1",
special: false,
token: "test01",
valid: true,
validTill: moment().add("10", "years").toDate(),
user: u._id,
validated: true
});
await LoginToken.save(login_token);
let special_token = await LoginToken.findOne({ token: "test02" });
if (special_token)
await LoginToken.delete(special_token);
special_token = LoginToken.new({
browser: "DEMO",
ip: "10.0.0.1",
special: true,
token: "test02",
valid: true,
validTill: moment().add("10", "years").toDate(),
user: u._id,
validated: true
});
await LoginToken.save(special_token);
// setInterval(() => {
// let code = speakeasy.totp({
// secret: t.data,
// encoding: "base32"
// })
// Logging.debug("OTC Code is:", code);
// }, 1000)
} }

View File

@ -1,101 +1,101 @@
import { WebConfig } from "./config"; import { WebConfig } from "./config";
import * as express from "express" import * as express from "express"
import { Express } from "express" import { Express } from "express"
import Logging from "@hibas123/nodelogging" import Logging from "@hibas123/nodelogging"
import * as bodyparser from "body-parser"; import * as bodyparser from "body-parser";
import * as cookieparser from "cookie-parser" import * as cookieparser from "cookie-parser"
import * as i18n from "i18n" import * as i18n from "i18n"
import * as compression from "compression"; import * as compression from "compression";
import ApiRouter from "./api"; import ApiRouter from "./api";
import ViewRouter from "./views/views"; import ViewRouter from "./views/views";
import RequestError, { HttpStatusCode } from "./helper/request_error"; import RequestError, { HttpStatusCode } from "./helper/request_error";
export default class Web { export default class Web {
server: Express server: Express
private port: number private port: number
constructor(config: WebConfig) { constructor(config: WebConfig) {
this.server = express() this.server = express()
this.port = Number(config.port); this.port = Number(config.port);
this.registerMiddleware() this.registerMiddleware()
this.registerEndpoints() this.registerEndpoints()
this.registerErrorHandler() this.registerErrorHandler()
} }
listen() { listen() {
this.server.listen(this.port, () => { this.server.listen(this.port, () => {
Logging.log(`Server listening on port ${this.port}`) Logging.log(`Server listening on port ${this.port}`)
}) })
} }
private registerMiddleware() { private registerMiddleware() {
this.server.use(cookieparser()) this.server.use(cookieparser())
this.server.use(bodyparser.json(), bodyparser.urlencoded({ extended: true })) this.server.use(bodyparser.json(), bodyparser.urlencoded({ extended: true }))
this.server.use(i18n.init) this.server.use(i18n.init)
//Logging Middleware //Logging Middleware
this.server.use((req, res, next) => { this.server.use((req, res, next) => {
let start = process.hrtime() let start = process.hrtime()
let finished = false; let finished = false;
let to = false; let to = false;
let listener = () => { let listener = () => {
if (finished) return; if (finished) return;
finished = true; finished = true;
let td = process.hrtime(start); let td = process.hrtime(start);
let time = !to ? (td[0] * 1e3 + td[1] / 1e6).toFixed(2) : "--.--"; let time = !to ? (td[0] * 1e3 + td[1] / 1e6).toFixed(2) : "--.--";
let resColor = ""; let resColor = "";
if (res.statusCode >= 200 && res.statusCode < 300) resColor = "\x1b[32m" //Green if (res.statusCode >= 200 && res.statusCode < 300) resColor = "\x1b[32m" //Green
else if (res.statusCode === 304 || res.statusCode === 302) resColor = "\x1b[33m" else if (res.statusCode === 304 || res.statusCode === 302) resColor = "\x1b[33m"
else if (res.statusCode >= 400 && res.statusCode < 500) resColor = "\x1b[36m" //Cyan else if (res.statusCode >= 400 && res.statusCode < 500) resColor = "\x1b[36m" //Cyan
else if (res.statusCode >= 500 && res.statusCode < 600) resColor = "\x1b[31m" //Red else if (res.statusCode >= 500 && res.statusCode < 600) resColor = "\x1b[31m" //Red
let m = req.method; let m = req.method;
while (m.length < 4) m += " "; while (m.length < 4) m += " ";
Logging.log(`${m} ${req.originalUrl} ${req.language} ${resColor}${res.statusCode}\x1b[0m - ${time}ms`) Logging.log(`${m} ${req.originalUrl} ${req.language} ${resColor}${res.statusCode}\x1b[0m - ${time}ms`)
res.removeListener("finish", listener) res.removeListener("finish", listener)
} }
res.on("finish", listener) res.on("finish", listener)
setTimeout(() => { setTimeout(() => {
to = true; to = true;
listener(); listener();
}, 2000) }, 2000)
next() next()
}) })
this.server.use(compression({ this.server.use(compression({
filter: (req, res) => { filter: (req, res) => {
if (req.headers['x-no-compression']) { if (req.headers['x-no-compression']) {
return false return false
} }
return compression.filter(req, res) return compression.filter(req, res)
} }
})); }));
} }
private registerEndpoints() { private registerEndpoints() {
this.server.use("/api", ApiRouter); this.server.use("/api", ApiRouter);
this.server.use("/", ViewRouter) this.server.use("/", ViewRouter)
} }
private registerErrorHandler() { private registerErrorHandler() {
this.server.use((error, req: express.Request, res, next) => { this.server.use((error, req: express.Request, res, next) => {
if (!(error instanceof RequestError)) { if (!(error instanceof RequestError)) {
error = new RequestError(error.message, error.status || HttpStatusCode.INTERNAL_SERVER_ERROR, error.nolog || false); error = new RequestError(error.message, error.status || HttpStatusCode.INTERNAL_SERVER_ERROR, error.nolog || false);
} }
if (error.status === 500 && !(<any>error).nolog) { if (error.status === 500 && !(<any>error).nolog) {
Logging.error(error); Logging.error(error);
} else { } else {
Logging.log(typeof error.message === "string" ? error.message.split("\n", 1)[0] : error.message); Logging.log("Responded with Error:", typeof error.message === "string" ? error.message.split("\n", 1)[0] : error.message);
} }
if (req.accepts(["json"])) { if (req.accepts(["json"])) {
res.json_status = error.status || 500; res.json_status = error.status || 500;
res.json({ error: error.message, status: error.status || 500 }) res.json({ error: error.message, status: error.status || 500, additional: error.additional })
} else } else
res.status(error.status || 500).send(error.message) res.status(error.status || 500).send(error.message)
}) })
} }
} }