First version of OpenAuth remake

This commit is contained in:
Fabian Stamm 2018-11-06 20:48:50 +01:00
commit ac69e73344
89 changed files with 14355 additions and 0 deletions

8
.drone.yml Normal file
View File

@ -0,0 +1,8 @@
pipeline:
core:
image: node
commands:
- node --version && npm --version
- npm install
- cd views && npm install && cd ..
- npm run build

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 3
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules/
views/out/
lib/
keys/
*.old
logs/
*.sqlite
yarn-error\.log
config.ini

1
README.md Normal file
View File

@ -0,0 +1 @@
OpenAuthService

11
example.config.ini Normal file
View File

@ -0,0 +1,11 @@
[core]
name = OpenAuthService
[web]
port = 3000
[mail]
server = mail.stamm.me
username = test
password = test
port = 595

36
locales/de.json Normal file
View File

@ -0,0 +1,36 @@
{
"User not found": "Benutzer nicht gefunden",
"Password or username wrong": "Passwort oder Benutzername falsch",
"Authorize %s": "Authorize %s",
"Login": "Einloggen",
"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",
"Password": "Passwort",
"Next": "Weiter",
"Register": "Registrieren",
"Mail": "Mail",
"Repeat Password": "Passwort wiederholen",
"Username": "Benutzername",
"Name": "Name",
"Registration code": "Registrierungs Schlüssel",
"You need to select one of the options": "Du musst eine der Optionen auswälen",
"Male": "Mann",
"Female": "Frau",
"Other": "Anderes",
"Registration code required": "Registrierungs Schlüssel benötigt",
"Username required": "Benutzername benötigt",
"Name required": "Name benötigt",
"Mail required": "Mail benötigt",
"The passwords do not match": "Die Passwörter stimmen nicht überein",
"Password is required": "Password benötigt",
"Invalid registration code": "Ungültiger Registrierungs Schlüssel",
"Username taken": "Benutzername nicht verfügbar",
"Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden",
"Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet",
"Administration": "Administration",
"Field {{field}} is not defined": "Feld {{field}} ist nicht deklariert",
"Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Type. Es sollte vom Typ {{type}} sein",
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
"Invalid token": "Ungültiges 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."
}

7
locales/en.json Normal file
View File

@ -0,0 +1,7 @@
{
"Login": "Login",
"Username or Email": "Username or Email",
"Password": "Password",
"Next": "Next",
"Invalid code": "Invalid code"
}

4592
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "open_auth_service",
"version": "1.0.0",
"main": "lib/index.js",
"author": "Fabian Stamm <dev@fabianstamm.de>",
"license": "MIT",
"scripts": {
"start": "node lib/index.js",
"build": "tsc && cd views && npm run build && cd ..",
"watch-ts": "tsc -w",
"watch-views": "cd views && npm run watch",
"watch-node": "nodemon --ignore ./views lib/index.js",
"watch": "concurrently \"npm:watch-*\""
},
"devDependencies": {
"@types/body-parser": "^1.17.0",
"@types/compression": "^0.0.36",
"@types/cookie-parser": "^1.4.1",
"@types/dotenv": "^4.0.3",
"@types/express": "^4.16.0",
"@types/handlebars": "^4.0.39",
"@types/i18n": "^0.8.3",
"@types/ini": "^1.3.29",
"@types/jsonwebtoken": "^8.3.0",
"@types/mongodb": "^3.1.14",
"@types/node": "^10.12.2",
"@types/node-rsa": "^0.4.3",
"@types/uuid": "^3.4.4",
"concurrently": "^4.0.1",
"nodemon": "^1.18.6",
"typescript": "^3.1.6"
},
"dependencies": {
"@hibas123/nodelogging": "^1.3.21",
"@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/safe_mongo": "^1.3.3",
"body-parser": "^1.18.3",
"compression": "^1.7.3",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"dotenv": "^6.1.0",
"express": "^4.16.4",
"handlebars": "^4.0.12",
"i18n": "^0.8.3",
"ini": "^1.3.5",
"jsonwebtoken": "^8.3.0",
"moment": "^2.22.2",
"mongodb": "^3.1.9",
"node-rsa": "^1.0.1",
"reflect-metadata": "^0.1.12",
"tedious": "^2.6.4",
"uuid": "^3.3.2"
}
}

12
src/api/admin.ts Normal file
View File

@ -0,0 +1,12 @@
import { Request, Router } from "express";
import ClientRoute from "./admin/client";
import UserRoute from "./admin/user";
import RegCodeRoute from "./admin/regcode";
import PermissionRoute from "./admin/permission";
const AdminRoute: Router = Router();
AdminRoute.use("/client", ClientRoute);
AdminRoute.use("/regcode", RegCodeRoute)
AdminRoute.use("/user", UserRoute)
AdminRoute.use("/permission", PermissionRoute);
export default AdminRoute;

88
src/api/admin/client.ts Normal file
View File

@ -0,0 +1,88 @@
import { Router, Request } from "express";
import { GetUserMiddleware } from "../middlewares/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import Client from "../../models/client";
import User from "../../models/user";
import verify, { Types } from "../middlewares/verify";
import { randomBytes } from "crypto";
const ClientRouter: Router = Router();
ClientRouter.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
ClientRouter.route("/")
.get(promiseMiddleware(async (req, res) => {
let clients = await Client.find({});
//ToDo check if user is required!
res.json(clients);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await Client.delete(id);
res.json({ success: true });
}))
.post(verify({
internal: {
type: Types.BOOLEAN,
optional: true
},
name: {
type: Types.STRING
},
redirect_url: {
type: Types.STRING
},
website: {
type: Types.STRING
},
logo: {
type: Types.STRING,
optional: true
}
}, true), promiseMiddleware(async (req, res) => {
req.body.client_secret = randomBytes(32).toString("hex");
let client = Client.new(req.body);
client.maintainer = req.user._id;
await Client.save(client)
res.json(client);
}))
.put(verify({
id: {
type: Types.STRING,
query: true
},
internal: {
type: Types.BOOLEAN,
optional: true
},
name: {
type: Types.STRING,
optional: true
},
redirect_url: {
type: Types.STRING,
optional: true
},
website: {
type: Types.STRING,
optional: true
},
logo: {
type: Types.STRING,
optional: true
}
}, true), promiseMiddleware(async (req, res) => {
let { id } = req.query;
let client = await Client.findById(id);
if (!client) throw new RequestError(req.__("Client not found"), HttpStatusCode.BAD_REQUEST);
for (let key in req.body) {
client[key] = req.body[key];
}
await Client.save(client);
res.json(client);
}))
export default ClientRouter;

View File

@ -0,0 +1,45 @@
import { Request, Router } from "express";
import { GetUserMiddleware } from "../middlewares/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import Permission from "../../models/permissions";
import verify, { Types } from "../middlewares/verify";
import Client from "../../models/client";
const PermissionRoute: Router = Router();
PermissionRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
PermissionRoute.route("/")
.get(promiseMiddleware(async (req, res) => {
let permission = await Permission.find({});
res.json(permission);
}))
.post(verify({
clientId: {
type: Types.NUMBER
},
name: {
type: Types.STRING
},
description: {
type: Types.STRING
}
}, true), promiseMiddleware(async (req, res) => {
let client = await Client.findById(req.body.clientId);
if (!client) {
throw new RequestError("Client not found", HttpStatusCode.BAD_REQUEST);
}
let permission = Permission.new({
description: req.body.description,
name: req.body.name,
client: client._id
});
await Permission.save(permission);
res.json(permission);
}))
export default PermissionRoute;

34
src/api/admin/regcode.ts Normal file
View File

@ -0,0 +1,34 @@
import { Request, Router } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware";
import RegCode from "../../models/regcodes";
import { randomBytes } from "crypto";
import moment = require("moment");
import { GetUserMiddleware } from "../middlewares/user";
import { HttpStatusCode } from "../../helper/request_error";
const RegCodeRoute: Router = Router();
RegCodeRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
RegCodeRoute.route("/")
.get(promiseMiddleware(async (req, res) => {
let regcodes = await RegCode.find({});
res.json(regcodes);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await RegCode.delete(id);
res.json({ success: true });
}))
.post(promiseMiddleware(async (req, res) => {
let regcode = RegCode.new({
token: randomBytes(10).toString("hex"),
valid: true,
validTill: moment().add("1", "month").toDate()
})
await RegCode.save(regcode);
res.json({ code: regcode.token });
}))
export default RegCodeRoute;

42
src/api/admin/user.ts Normal file
View File

@ -0,0 +1,42 @@
import { Request, Router } from "express";
import { GetUserMiddleware } from "../middlewares/user";
import { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import User from "../../models/user";
import Mail from "../../models/mail";
import RefreshToken from "../../models/refresh_token";
import LoginToken from "../../models/login_token";
const UserRoute: Router = Router();
UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
})
UserRoute.route("/")
.get(promiseMiddleware(async (req, res) => {
let users = await User.find({});
res.json(users);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
let user = await User.findById(id);
await Promise.all([
user.mails.map(mail => Mail.delete(mail)),
[
RefreshToken.deleteFilter({ user: user._id }),
LoginToken.deleteFilter({ user: user._id })
]
])
await User.delete(user);
res.json({ success: true });
})).put(promiseMiddleware(async (req, res) => {
let { id } = req.query;
let user = await User.findById(id);
user.admin = !user.admin;
await User.save(user);
res.json({ success: true })
}))
export default UserRoute;

18
src/api/api.ts Normal file
View File

@ -0,0 +1,18 @@
import * as express from "express"
import AdminRoute from "./admin";
import UserRoute from "./user";
import InternalRoute from "./internal";
import Login from "./user/login";
import { AuthGetUser } from "./client/user";
const ApiRouter: express.IRouter<void> = express.Router();
ApiRouter.use("/admin", AdminRoute);
ApiRouter.use("/user", UserRoute);
ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/user", AuthGetUser);
// Legacy reasons (deprecated)
ApiRouter.post("/login", Login);
export default ApiRouter;

0
src/api/client.ts Normal file
View File

14
src/api/client/user.ts Normal file
View File

@ -0,0 +1,14 @@
import { Request, Response } from "express"
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys";
export const AuthGetUser = Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(true, false), async (req: Request, res: Response) => {
let jwt = await createJWT({
client: req.client.client_id,
uid: req.user.uid,
username: req.user.username
}, 30); //after 30 seconds this token is invalid
res.redirect(req.query.redirect_uri + "?jwt=" + jwt)
});

8
src/api/internal.ts Normal file
View File

@ -0,0 +1,8 @@
import { Router } from "express";
import { OAuthInternalApp } from "./internal/oauth";
import PasswordAuth from "./internal/password";
const InternalRoute: Router = Router();
InternalRoute.get("/oauth", OAuthInternalApp);
InternalRoute.post("/password", PasswordAuth)
export default InternalRoute;

29
src/api/internal/oauth.ts Normal file
View File

@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from "express";
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import { UserMiddleware } from "../middlewares/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import ClientCode from "../../models/client_code";
import moment = require("moment");
import { randomBytes } from "crypto";
export const OAuthInternalApp = Stacker(GetClientAuthMiddleware(false, true), UserMiddleware,
async (req: Request, res: Response) => {
let { redirect_uri, state } = req.query
if (!redirect_uri) {
throw new RequestError("No redirect url set!", HttpStatusCode.BAD_REQUEST);
}
let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&";
let code = ClientCode.new({
user: req.user._id,
client: req.client._id,
validTill: moment().add(30, "minutes").toDate(),
code: randomBytes(16).toString("hex"),
permissions: []
});
await ClientCode.save(code);
res.redirect(redirect_uri + sep + "code=" + code.code + (state ? "&state=" + state : ""));
res.end();
});

View File

@ -0,0 +1,25 @@
import { Request, Response, NextFunction } from "express";
import { GetClientAuthMiddleware } from "../middlewares/client";
import Stacker from "../middlewares/stacker";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user";
const PasswordAuth = Stacker(GetClientAuthMiddleware(true, true), async (req: Request, res: Response) => {
let { username, password, uid }: { username: string, password: string, uid: string } = req.body;
let query: any = { password: password };
if (username) {
query.username = username.toLowerCase()
} else if (uid) {
query.uid = uid;
} else {
throw new RequestError(req.__("No username or uid set"), HttpStatusCode.BAD_REQUEST);
}
let user = await User.findOne(query);
if (!user) {
res.json({ error: req.__("Password or username wrong") })
} else {
res.json({ success: true, uid: user.uid });
}
});
export default PasswordAuth;

View File

@ -0,0 +1,76 @@
import { NextFunction, Request, Response } from "express";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import Client from "../../models/client";
import { validateJWT } from "../../keys";
import User from "../../models/user";
import Mail from "../../models/mail";
import { OAuthJWT } from "../../helper/jwt";
export function GetClientAuthMiddleware(checksecret = true, internal = false, checksecret_if_available = false) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
let client_id = req.query.client_id || req.body.client_id;
let client_secret = req.query.client_secret || req.body.client_secret;
if (!client_id || (!client_secret && checksecret)) {
throw new RequestError("No client credentials", HttpStatusCode.BAD_REQUEST);
}
let w = { client_id: client_id, client_secret: client_secret };
if (!checksecret && !(checksecret_if_available && client_secret)) delete w.client_secret;
let client = await Client.findOne(w)
if (!client) {
throw new RequestError("Invalid client_id" + (checksecret ? "or client_secret" : ""), HttpStatusCode.BAD_REQUEST);
}
if (internal && !client.internal) {
throw new RequestError(req.__("Client has no permission for access"), HttpStatusCode.FORBIDDEN)
}
req.client = client;
next();
} catch (e) {
if (next) next(e);
else throw e;
}
}
}
export const ClientAuthMiddleware = GetClientAuthMiddleware();
export function GetClientApiAuthMiddleware(permissions?: string[]) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const invalid_err = new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED);
let token = req.query.access_token || req.headers.authorization;
if (!token)
throw invalid_err;
let data: OAuthJWT;
try {
data = await validateJWT(token);
} catch (err) {
throw invalid_err
}
let user = await User.findOne({ uid: data.user });
if (!user)
throw invalid_err;
let client = await Client.findOne({ client_id: data.application })
if (!client)
throw invalid_err;
if (permissions && (!data.permissions || !permissions.every(e => data.permissions.indexOf(e) >= 0)))
throw invalid_err;
req.user = user;
req.client = client;
next();
} catch (e) {
if (next) next(e);
else throw e;
}
}
}

View File

@ -0,0 +1,24 @@
import { Request, Response, NextFunction, RequestHandler } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware";
function call(handler: RequestHandler, req: Request, res: Response) {
return new Promise((yes, no) => {
let p = handler(req, res, (err) => {
if (err) no(err);
else yes();
})
if (p && p.catch) p.catch(err => no(err));
})
}
const Stacker = (...handler: RequestHandler[]) => {
return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => {
let hc = handler.concat();
while (hc.length > 0) {
let h = hc.shift();
await call(h, req, res);
}
next();
});
}
export default Stacker;

View File

@ -0,0 +1,83 @@
import { NextFunction, Request, Response } from "express";
import LoginToken from "../../models/login_token";
import Logging from "@hibas123/nodelogging";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user";
import promiseMiddleware from "../../helper/promiseMiddleware";
class Invalid extends Error { }
/**
* 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
* 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 redirect_uri Sets the uri to redirect, if json is not set and user not logged in
*/
export function GetUserMiddleware(json = false, special_token: boolean = false, redirect_uri?: string) {
return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) {
const invalid = () => {
throw new Invalid();
}
try {
let { login, special } = req.cookies
if (!login) invalid()
let token = await LoginToken.findOne({ token: login, valid: true })
if (!token) invalid()
let user = await User.findById(token.user);
if (!user) {
token.valid = false;
await LoginToken.save(token);
invalid();
}
if (token.validTill.getTime() < new Date().getTime()) { //Token expired
token.valid = false;
await LoginToken.save(token);
invalid()
}
if (special) {
Logging.debug("Special found")
let st = await LoginToken.findOne({ token: special, special: true, valid: true })
if (st && st.valid && st.user.toHexString() === token.user.toHexString()) {
if (st.validTill.getTime() < new Date().getTime()) { //Token expired
Logging.debug("Special expired")
st.valid = false;
await LoginToken.save(st);
} else {
Logging.debug("Special valid")
req.special = true;
}
}
}
if (special_token && !req.special) invalid();
req.user = user
req.isAdmin = user.admin;
if (next)
next()
return true;
} catch (e) {
if (e instanceof Invalid) {
if (req.method === "GET" && !json) {
res.status(HttpStatusCode.UNAUTHORIZED)
res.redirect("/login?base64=true&state=" + new Buffer(redirect_uri ? redirect_uri : req.originalUrl).toString("base64"))
} else {
throw new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED)
}
} else {
if (next) next(e);
else throw e;
}
return false;
}
});
}
export const UserMiddleware = GetUserMiddleware();

View File

@ -0,0 +1,125 @@
import { Request, Response, NextFunction } from "express"
import { Logging } from "@hibas123/nodelogging";
import { isBoolean, isString, isNumber, isObject, isDate, isArray, isSymbol } from "util";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
export enum Types {
STRING,
NUMBER,
BOOLEAN,
EMAIL,
OBJECT,
DATE,
ARRAY,
ENUM
}
function isEmail(value: any): boolean {
return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value)
}
export interface CheckObject {
type: Types
query?: boolean
optional?: boolean
/**
* Only when Type.ENUM
*
* values to check before
*/
values?: string[]
/**
* Only when Type.STRING
*/
notempty?: boolean // Only STRING
}
export interface Checks {
[index: string]: CheckObject// | Types
}
// req: Request, res: Response, next: NextFunction
export default function (fields: Checks, noadditional = false) {
return (req: Request, res: Response, next: NextFunction) => {
let errors: { message: string, field: string }[] = []
function check(data: any, field_name: string, field: CheckObject) {
if (data !== undefined && data !== null) {
switch (field.type) {
case Types.STRING:
if (isString(data)) {
if (!field.notempty) return;
if (data !== "") return;
}
break;
case Types.NUMBER:
if (isNumber(data)) return;
break;
case Types.EMAIL:
if (isEmail(data)) return;
break;
case Types.BOOLEAN:
if (isBoolean(data)) return;
break;
case Types.OBJECT:
if (isObject(data)) return;
break;
case Types.ARRAY:
if (isArray(data)) return;
break;
case Types.DATE:
if (isDate(data)) return;
break;
case Types.ENUM:
if (isString(data)) {
if (field.values.indexOf(data) >= 0) return;
}
break;
default:
Logging.error(`Invalid type to check: ${field.type} ${Types[field.type]}`)
}
errors.push({
message: res.__("Field {{field}} has wrong type. It should be from type {{type}}", { field: field_name, type: Types[field.type].toLowerCase() }),
field: field_name
})
} else {
if (!field.optional) errors.push({
message: res.__("Field {{field}} is not defined", { field: field_name }),
field: field_name
})
}
}
for (let field_name in fields) {
let field = fields[field_name]
let data = fields[field_name].query ? req.query[field_name] : req.body[field_name]
check(data, field_name, field)
}
if (noadditional) { //Checks if the data given has additional parameters
let should = Object.keys(fields);
should = should.filter(e => !fields[e].query); //Query parameters should not exist on body
let has = Object.keys(req.body);
has.every(e => {
if (should.indexOf(e) >= 0) {
return true;
} else {
errors.push({
message: res.__("Field {{field}} should not be there", { field: e }),
field: e
})
return false;
}
})
}
if (errors.length > 0) {
let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true);
next(err);
} else
next()
}
}

13
src/api/oauth.ts Normal file
View File

@ -0,0 +1,13 @@
import { Router } from "express";
import AuthRoute from "./oauth/auth";
import JWTRoute from "./oauth/jwt";
import Public from "./oauth/public";
import RefreshTokenRoute from "./oauth/refresh";
const OAuthRoue: Router = Router();
OAuthRoue.post("/auth", AuthRoute);
OAuthRoue.get("/jwt", JWTRoute)
OAuthRoue.get("/public", Public)
OAuthRoue.get("/refresh", RefreshTokenRoute);
OAuthRoue.post("/refresh", RefreshTokenRoute);
export default OAuthRoue;

81
src/api/oauth/auth.ts Normal file
View File

@ -0,0 +1,81 @@
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import { Request, Response } from "express";
import Client from "../../models/client";
import Logging from "@hibas123/nodelogging";
import Permission, { IPermission } from "../../models/permissions";
import { Sequelize } from "sequelize-typescript";
import ClientCode from "../../models/client_code";
import moment = require("moment");
import { randomBytes } from "crypto";
import { ObjectID } from "bson";
const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => {
let { response_type, client_id, redirect_uri, scope, state, nored } = req.query;
const sendError = (type) => {
res.redirect(redirect_uri += `?error=${type}&state=${state}`);
}
/**
* error
REQUIRED. A single ASCII [USASCII] error code from the
following:
invalid_request
The request is missing a required parameter, includes an
invalid parameter value, includes a parameter more than
once, or is otherwise malformed.
unauthorized_client
The client is not authorized to request an authorization
code using this method.
access_denied
The resource owner or authorization server denied the
request.
*/
try {
if (response_type !== "code") {
return sendError("unsupported_response_type");
} else {
let client = await Client.findOne({ client_id: client_id })
if (!client) {
return sendError("unauthorized_client")
}
if (redirect_uri && client.redirect_url !== redirect_uri) {
Logging.log(redirect_uri, client.redirect_url);
return res.send("Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!");
}
let permissions: IPermission[] = [];
if (scope) {
let perms = (<string>scope).split(";").map(p => new ObjectID(p));
permissions = await Permission.find({ _id: { $in: perms } })
if (permissions.length != perms.length) {
return sendError("invalid_scope");
}
}
let code = ClientCode.new({
user: req.user._id,
client: client._id,
permissions: permissions.map(p => p._id),
validTill: moment().add(30, "minutes").toDate(),
code: randomBytes(16).toString("hex")
});
await ClientCode.save(code);
let ruri = client.redirect_url + `?code=${code.code}&state=${state}`;
if (nored === "true") {
res.json({
redirect_uri: ruri
})
} else {
res.redirect(ruri);
}
}
} catch (err) {
Logging.error(err);
sendError("server_error")
}
})
export default AuthRoute;

27
src/api/oauth/jwt.ts Normal file
View File

@ -0,0 +1,27 @@
import { Request, Response } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import RefreshToken from "../../models/refresh_token";
import User from "../../models/user";
import Permission from "../../models/permissions";
import Client from "../../models/client";
import getOAuthJWT from "../../helper/jwt";
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
let { refreshtoken } = req.query;
if (!refreshtoken) throw new RequestError(req.__("Refresh token not set"), HttpStatusCode.BAD_REQUEST);
let token = await RefreshToken.findOne({ where: { token: refreshtoken }, include: [User, Permission, Client] });
if (!token) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
let user = await User.findById(token.user);
if (!user) {
//TODO handle error!
}
let client = await Client.findById(token.client);
let jwt = await getOAuthJWT({ user, permissions: token.permissions, client });
res.json({ token: jwt });
})
export default JWTRoute;

6
src/api/oauth/public.ts Normal file
View File

@ -0,0 +1,6 @@
import { Request, Response } from "express";
import { public_key } from "../../keys";
export default function Public(req: Request, res: Response) {
res.json({ public_key: public_key })
}

79
src/api/oauth/refresh.ts Normal file
View File

@ -0,0 +1,79 @@
import { Request, Response } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user";
import Permission from "../../models/permissions";
import Client from "../../models/client";
import getOAuthJWT from "../../helper/jwt";
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client"
import ClientCode from "../../models/client_code";
import Mail from "../../models/mail";
import { randomBytes } from "crypto";
import moment = require("moment");
import { JWTExpDur } from "../../keys";
import RefreshToken from "../../models/refresh_token";
import { Promise } from "bluebird";
const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), async (req: Request, res: Response) => {
let code = req.query.code || req.body.code;
let redirect_uri = req.query.redirect_uri || req.body.redirect_uri;
let grant_type = req.query.grant_type || req.body.grant_type;
if (!grant_type || grant_type === "authorization_code") {
let c = await ClientCode.findOne({ where: { code: code }, include: [{ model: User, include: [Mail] }, Client, Permission] })
if (!c) {
throw new RequestError(req.__("Invalid code"), HttpStatusCode.BAD_REQUEST);
}
let client = await Client.findById(c.client);
let user = await User.findById(c.user);
let mails = await Promise.all(user.mails.map(m => Mail.findOne(m)));
let token = RefreshToken.new({
user: c.user,
client: c.client,
permissions: c.permissions,
token: randomBytes(16).toString("hex"),
valid: true,
validTill: moment().add(6, "months").toDate()
});
await RefreshToken.save(token);
await ClientCode.delete(c);
let mail = mails.find(e => e.primary);
if (!mail) mail = mails[0];
res.json({
refresh_token: token.token,
token: token.token,
access_token: await getOAuthJWT({
client: client,
user: user,
permissions: c.permissions
}),
token_type: "bearer",
expires_in: JWTExpDur.asSeconds(),
profile: {
uid: user.uid,
email: mail ? mail.mail : "",
name: user.name,
}
});
} else if (grant_type === "refresh_token") {
let refresh_token = req.query.refresh_token || req.body.refresh_token;
if (!refresh_token) throw new RequestError(req.__("refresh_token not set"), HttpStatusCode.BAD_REQUEST);
let token = await RefreshToken.findOne({ token: refresh_token });
if (!token) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
let user = await User.findById(token.user);
let client = await Client.findById(token.client)
let jwt = await getOAuthJWT({ user, client, permissions: token.permissions });
res.json({ access_token: jwt, expires_in: JWTExpDur.asSeconds() });
} else {
throw new RequestError("invalid grant_type", HttpStatusCode.BAD_REQUEST);
}
})
export default RefreshTokenRoute;

8
src/api/user.ts Normal file
View File

@ -0,0 +1,8 @@
import { Request, Router } from "express";
import Register from "./user/register";
import Login from "./user/login";
const UserRoute: Router = Router();
UserRoute.post("/register", Register);
UserRoute.post("/login", Login)
export default UserRoute;

81
src/api/user/login.ts Normal file
View File

@ -0,0 +1,81 @@
import { Request, Response } from "express"
import User, { IUser } from "../../models/user";
import { randomBytes } from "crypto";
import moment = require("moment");
import LoginToken from "../../models/login_token";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
const Login = promiseMiddleware(async (req: Request, res: Response) => {
let type = req.query.type;
if (type === "username") {
let { username, uid } = req.query;
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid });
if (!user) {
res.json({ error: req.__("User not found") })
} else {
res.json({ salt: user.salt, uid: user.uid });
}
return;
}
const sendToken = async (user: IUser) => {
let token_str = randomBytes(16).toString("hex");
let token_exp = moment().add(6, "months").toDate()
let token = LoginToken.new({
token: token_str,
valid: true,
validTill: token_exp,
user: user._id
});
await LoginToken.save(token);
let special_str = randomBytes(24).toString("hex");
let special_exp = moment().add(30, "minutes").toDate()
let special = LoginToken.new({
token: special_str,
valid: true,
validTill: special_exp,
special: true,
user: user._id
});
await LoginToken.save(special);
res.json({
login: { token: token_str, expires: token_exp.toUTCString() },
special: { token: special_str, expires: special_exp.toUTCString() }
});
}
if (type === "password" || type === "twofactor") {
let { username, password, uid } = req.body;
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
if (!user) {
res.json({ error: req.__("User not found") })
} else {
if (user.password !== password) {
res.json({ error: req.__("Password or username wrong") })
} else {
if (type === "twofactor") {
} else {
if (user.twofactor && user.twofactor.length > 0) {
let types = user.twofactor.map(f => {
return { type: f.type };
})
res.json({
types: types
});
} else {
await sendToken(user);
}
}
}
}
} else {
throw new RequestError("Invalid type!", HttpStatusCode.BAD_REQUEST);
}
});
export default Login;

142
src/api/user/register.ts Normal file
View File

@ -0,0 +1,142 @@
import { Request, Response, Router } from "express"
import Stacker from "../middlewares/stacker";
import verify, { Types } from "../middlewares/verify";
import promiseMiddleware from "../../helper/promiseMiddleware";
import User, { Gender } from "../../models/user";
import { HttpStatusCode } from "../../helper/request_error";
import Mail from "../../models/mail";
import RegCode from "../../models/regcodes";
const Register = Stacker(verify({
mail: {
type: Types.EMAIL,
notempty: true
},
username: {
type: Types.STRING,
notempty: true
},
password: {
type: Types.STRING,
notempty: true
},
salt: {
type: Types.STRING,
notempty: true
},
regcode: {
type: Types.STRING,
notempty: true
},
gender: {
type: Types.STRING,
notempty: true
},
name: {
type: Types.STRING,
notempty: true
},
// birthday: {
// type: Types.DATE
// }
}), promiseMiddleware(async (req: Request, res: Response) => {
let { username, password, salt, mail, gender, name, birthday, regcode } = req.body;
let u = await User.findOne({ username: username.toLowerCase() })
if (u) {
let err = {
message: [
{
message: req.__("Username taken"),
field: "username"
}
],
status: HttpStatusCode.BAD_REQUEST,
nolog: true
}
throw err;
}
let m = await Mail.findOne({ mail: mail })
if (m) {
let err = {
message: [
{
message: req.__("Mail linked with other account"),
field: "mail"
}
],
status: HttpStatusCode.BAD_REQUEST,
nolog: true
}
throw err;
}
let regc = await RegCode.findOne({ token: regcode })
if (!regc) {
let err = {
message: [
{
message: req.__("Invalid registration code"),
field: "regcode"
}
],
status: HttpStatusCode.BAD_REQUEST,
nolog: true
}
throw err;
}
if (!regc.valid) {
let err = {
message: [
{
message: req.__("Registration code already used"),
field: "regcode"
}
],
status: HttpStatusCode.BAD_REQUEST,
nolog: true
}
throw err;
}
let g = -1;
switch (gender) {
case "male":
g = Gender.male
break;
case "female":
g = Gender.female
break;
case "other":
g = Gender.other
break;
default:
g = Gender.none
break;
}
let user = User.new({
username: username.toLowerCase(),
password: password,
salt: salt,
gender: g,
name: name,
// birthday: birthday,
admin: false
})
regc.valid = false;
await RegCode.save(regc);
let ml = Mail.new({
mail: mail,
primary: true
})
user.mails.push(ml._id);
await User.save(user)
res.json({ success: true });
}))
export default Register;

46
src/config.ts Normal file
View File

@ -0,0 +1,46 @@
export interface DatabaseConfig {
host: string
database: string
dialect: "sqlite" | "mysql" | "postgres" | "mssql"
username: string
password: string
storage: string
benchmark: "true" | "false" | undefined
}
export interface WebConfig {
port: string
secure: "true" | "false" | undefined
}
export interface CoreConfig {
name: string
}
export interface Config {
core: CoreConfig
database: DatabaseConfig
web: WebConfig
dev: boolean
logging: {
server: string;
appid: string;
token: string;
} | undefined
}
import * as ini from "ini";
import { readFileSync } from "fs";
import * as dotenv from "dotenv";
import { Logging } from "@hibas123/nodelogging";
dotenv.config();
const config: Config = ini.parse(readFileSync("./config.ini").toString())
if (process.env.DEV === "true") {
config.dev = true;
Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ")
}
export default config;

3
src/database.ts Normal file
View File

@ -0,0 +1,3 @@
import SafeMongo from "@hibas123/safe_mongo";
const DB = new SafeMongo("mongodb://localhost", "openauth");
export default DB;

11
src/express.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import { IUser } from "./models/user";
import { IClient } from "./models/client";
declare module "express" {
interface Request {
user: IUser;
client: IClient;
isAdmin: boolean;
special: boolean;
}
}

20
src/helper/jwt.ts Normal file
View File

@ -0,0 +1,20 @@
import { IUser } from "../models/user";
import { ObjectID } from "bson";
import { createJWT } from "../keys";
import { IClient } from "../models/client";
export interface OAuthJWT {
user: string;
username: string;
permissions: string[];
application: string
}
export default function getOAuthJWT(token: { user: IUser, permissions: ObjectID[], client: IClient }) {
return createJWT(<OAuthJWT>{
user: token.user.uid,
username: token.user.username,
permissions: token.permissions.map(p => p.toHexString()),
application: token.client.client_id
})
}

View File

@ -0,0 +1,5 @@
import { Request, Response, NextFunction } from "express"
export default (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next)
}

388
src/helper/request_error.ts Normal file
View File

@ -0,0 +1,388 @@
/**
* Hypertext Transfer Protocol (HTTP) response status codes.
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
*/
export enum HttpStatusCode {
/**
* 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).
* 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
* 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,
/**
* The requester has asked the server to switch protocols and the server has agreed to do so.
*/
SWITCHING_PROTOCOLS = 101,
/**
* 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 prevents the client from timing out and assuming the request was lost.
*/
PROCESSING = 102,
/**
* Standard response for successful HTTP requests.
* 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 POST request, the response will contain an entity describing or containing the result of the action.
*/
OK = 200,
/**
* The request has been fulfilled, resulting in the creation of a new resource.
*/
CREATED = 201,
/**
* 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.
*/
ACCEPTED = 202,
/**
* SINCE HTTP/1.1
* 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.
*/
NON_AUTHORITATIVE_INFORMATION = 203,
/**
* The server successfully processed the request and is not returning any content.
*/
NO_CONTENT = 204,
/**
* 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.
*/
RESET_CONTENT = 205,
/**
* 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,
* or split a download into multiple simultaneous streams.
*/
PARTIAL_CONTENT = 206,
/**
* 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.
*/
MULTI_STATUS = 207,
/**
* The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response,
* and are not being included again.
*/
ALREADY_REPORTED = 208,
/**
* 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.
*/
IM_USED = 226,
/**
* 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,
* to list files with different filename extensions, or to suggest word-sense disambiguation.
*/
MULTIPLE_CHOICES = 300,
/**
* This and all future requests should be directed to the given URI.
*/
MOVED_PERMANENTLY = 301,
/**
* 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 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
* to distinguish between the two behaviours. However, some Web applications and frameworks
* use the 302 status code as if it were the 303.
*/
FOUND = 302,
/**
* SINCE HTTP/1.1
* 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
* the server has received the data and should issue a redirect with a separate GET message.
*/
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.
* In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy.
*/
NOT_MODIFIED = 304,
/**
* SINCE HTTP/1.1
* 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.
*/
USE_PROXY = 305,
/**
* No longer used. Originally meant "Subsequent requests should use the specified proxy."
*/
SWITCH_PROXY = 306,
/**
* 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 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.
*/
TEMPORARY_REDIRECT = 307,
/**
* 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.
* So, for example, submitting a form to a permanently redirected resource may continue smoothly.
*/
PERMANENT_REDIRECT = 308,
/**
* 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).
*/
BAD_REQUEST = 400,
/**
* 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
* requested resource. See Basic access authentication and Digest access authentication. 401 semantically means
* "unauthenticated",i.e. the user does not have the necessary credentials.
*/
UNAUTHORIZED = 401,
/**
* 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.
* Google Developers API uses this status if a particular developer has exceeded the daily limit on requests.
*/
PAYMENT_REQUIRED = 402,
/**
* The request was valid, but the server is refusing action.
* The user might not have the necessary permissions for a resource.
*/
FORBIDDEN = 403,
/**
* The requested resource could not be found but may be available in the future.
* Subsequent requests by the client are permissible.
*/
NOT_FOUND = 404,
/**
* 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.
*/
METHOD_NOT_ALLOWED = 405,
/**
* The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request.
*/
NOT_ACCEPTABLE = 406,
/**
* The client must first authenticate itself with the proxy.
*/
PROXY_AUTHENTICATION_REQUIRED = 407,
/**
* The server timed out waiting for the request.
* 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."
*/
REQUEST_TIMEOUT = 408,
/**
* Indicates that the request could not be processed because of conflict in the request,
* such as an edit conflict between multiple simultaneous updates.
*/
CONFLICT = 409,
/**
* 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.
* 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.
* 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,
/**
* The request did not specify the length of its content, which is required by the requested resource.
*/
LENGTH_REQUIRED = 411,
/**
* The server does not meet one of the preconditions that the requester put on the request.
*/
PRECONDITION_FAILED = 412,
/**
* The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large".
*/
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,
* in which case it should be converted to a POST request.
* Called "Request-URI Too Long" previously.
*/
URI_TOO_LONG = 414,
/**
* 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.
*/
UNSUPPORTED_MEDIA_TYPE = 415,
/**
* 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.
* Called "Requested Range Not Satisfiable" previously.
*/
RANGE_NOT_SATISFIABLE = 416,
/**
* The server cannot meet the requirements of the Expect request-header field.
*/
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,
* 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.
*/
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).
*/
MISDIRECTED_REQUEST = 421,
/**
* The request was well-formed but was unable to be followed due to semantic errors.
*/
UNPROCESSABLE_ENTITY = 422,
/**
* The resource that is being accessed is locked.
*/
LOCKED = 423,
/**
* The request failed due to failure of a previous request (e.g., a PROPPATCH).
*/
FAILED_DEPENDENCY = 424,
/**
* The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field.
*/
UPGRADE_REQUIRED = 426,
/**
* The origin server requires the request to be conditional.
* Intended to prevent "the 'lost update' problem, where a client
* 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."
*/
PRECONDITION_REQUIRED = 428,
/**
* The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes.
*/
TOO_MANY_REQUESTS = 429,
/**
* The server is unwilling to process the request because either an individual header field,
* or all the header fields collectively, are too large.
*/
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
* that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451.
*/
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
/**
* A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.
*/
INTERNAL_SERVER_ERROR = 500,
/**
* 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).
*/
NOT_IMPLEMENTED = 501,
/**
* The server was acting as a gateway or proxy and received an invalid response from the upstream server.
*/
BAD_GATEWAY = 502,
/**
* The server is currently unavailable (because it is overloaded or down for maintenance).
* Generally, this is a temporary state.
*/
SERVICE_UNAVAILABLE = 503,
/**
* The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.
*/
GATEWAY_TIMEOUT = 504,
/**
* The server does not support the HTTP protocol version used in the request
*/
HTTP_VERSION_NOT_SUPPORTED = 505,
/**
* Transparent content negotiation for the request results in a circular reference.
*/
VARIANT_ALSO_NEGOTIATES = 506,
/**
* The server is unable to store the representation needed to complete the request.
*/
INSUFFICIENT_STORAGE = 507,
/**
* The server detected an infinite loop while processing the request.
*/
LOOP_DETECTED = 508,
/**
* Further extensions to the request are required for the server to fulfill it.
*/
NOT_EXTENDED = 510,
/**
* 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
* to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot).
*/
NETWORK_AUTHENTICATION_REQUIRED = 511
}
export default class RequestError extends Error {
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false) {
super("")
this.message = message;
}
}

72
src/index.ts Normal file
View File

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

56
src/jwt.ts Normal file
View File

@ -0,0 +1,56 @@
interface AccessToken {
}
interface IDToken {
/**
* REQUIRED. Issuer Identifier for the Issuer of the response. The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.
*/
iss: string
/**
* REQUIRED. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is a case sensitive string.
*/
sub: string
/**
* REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string.
*/
aud: string
/**
*
REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing. The processing of this parameter requires that the current date/time MUST be before the expiration date/time listed in the value. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. See RFC 3339 [RFC3339] for details regarding date/times in general and UTC in particular.
*/
exp: number
/**
* REQUIRED. Time at which the JWT was issued. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.
*/
iat: number
/**
* Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. When a max_age request is made or when auth_time is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL. (The auth_time Claim semantically corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] auth_time response parameter.)
*/
auth_time: number
/**
* String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. The nonce value is a case sensitive string.
*/
nonce: string
/**
* OPTIONAL. Authentication Context Class Reference. String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied. The value "0" indicates the End-User authentication did not meet the requirements of ISO/IEC 29115 [ISO29115] level 1. Authentication using a long-lived browser cookie, for instance, is one example where the use of "level 0" is appropriate. Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value. (This corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] nist_auth_level 0.) An absolute URI or an RFC 6711 [RFC6711] registered name SHOULD be used as the acr value; registered names MUST NOT be used with a different meaning than that which is registered. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. The acr value is a case sensitive string.
*/
acr?: string
/**
* OPTIONAL. Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication. For instance, values might indicate that both password and OTP authentication methods were used. The definition of particular values to be used in the amr Claim is beyond the scope of this specification. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. The amr value is an array of case sensitive strings.
*/
amr?: string[]
/**
* OPTIONAL. Authorized party - the party to which the ID Token was issued. If present, it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. It MAY be included even when the authorized party is the same as the sole audience. The azp value is a case sensitive string containing a StringOrURI value.
*/
azp?: string
}

75
src/keys.ts Normal file
View File

@ -0,0 +1,75 @@
import Logging from "@hibas123/nodelogging";
import * as fs from "fs"
let private_key: string;
let rsa: RSA;
export function sign(message: Buffer): Buffer {
return rsa.sign(message, "buffer")
}
export function verify(message: Buffer, signature: Buffer): boolean {
return rsa.verify(message, signature);
}
export let public_key: string;
import * as jwt from "jsonwebtoken";
import config from "./config";
import * as moment from "moment";
export const JWTExpDur = moment.duration(6, "h");
export function createJWT(data: any, expiration?: number) {
return new Promise<string>((resolve, reject) => {
return jwt.sign(data, private_key, {
expiresIn: expiration || JWTExpDur.asSeconds(),
issuer: config.core.name,
algorithm: "RS256"
}, (err, token) => {
if (err) reject(err)
else resolve(token)
});
})
}
export async function validateJWT(data: string) {
return new Promise<any>((resolve, reject) => {
jwt.verify(data, public_key, (err, valid) => {
if (err) reject(err)
else resolve(valid)
});
})
}
let create = false;
if (fs.existsSync("./keys")) {
if (fs.existsSync("./keys/private.pem")) {
if (fs.existsSync("./keys/public.pem")) {
Logging.log("Using existing private and public key")
private_key = fs.readFileSync("./keys/private.pem").toString("utf8")
public_key = fs.readFileSync("./keys/public.pem").toString("utf8")
if (!private_key || !public_key) {
create = true;
}
} else create = true;
} else create = true;
} else create = true;
import * as RSA from "node-rsa"
if (create === true) {
Logging.log("Started RSA Key gen")
let rsa = new RSA({ b: 4096 });
private_key = rsa.exportKey("private")
public_key = rsa.exportKey("public")
if (!fs.existsSync("./keys")) {
fs.mkdirSync("./keys")
}
fs.writeFileSync("./keys/private.pem", private_key)
fs.writeFileSync("./keys/public.pem", public_key)
Logging.log("Key pair generated")
}
rsa = new RSA(private_key, "private")
rsa.importKey(public_key, "public")

36
src/models/client.ts Normal file
View File

@ -0,0 +1,36 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IClient extends ModelDataBase {
maintainer: ObjectID
internal: boolean
name: string
redirect_url: string
website: string
logo?: string
client_id: string
client_secret: string
}
const Client = DB.addModel<IClient>({
name: "client",
versions: [
{
migration: () => { },
schema: {
maintainer: { type: ObjectID },
internal: { type: Boolean, default: false },
name: { type: String },
redirect_url: { type: String },
website: { type: String },
logo: { type: String, optional: true },
client_id: { type: String, default: () => v4() },
client_secret: { type: String }
}
}
]
})
export default Client;

27
src/models/client_code.ts Normal file
View File

@ -0,0 +1,27 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IClientCode extends ModelDataBase {
user: ObjectID
code: string;
client: ObjectID
permissions: ObjectID[]
validTill: Date;
}
const ClientCode = DB.addModel<IClientCode>({
name: "client_code",
versions: [{
migration: () => { },
schema: {
user: { type: ObjectID },
code: { type: String },
client: { type: ObjectID },
permissions: { type: Array },
validTill: { type: Date }
}
}]
});
export default ClientCode;

26
src/models/login_token.ts Normal file
View File

@ -0,0 +1,26 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
export interface ILoginToken extends ModelDataBase {
token: string;
special: boolean;
user: ObjectID;
validTill: Date;
valid: boolean;
}
const LoginToken = DB.addModel<ILoginToken>({
name: "login_token",
versions: [{
migration: () => { },
schema: {
token: { type: String },
special: { type: Boolean, default: () => false },
user: { type: ObjectID },
validTill: { type: Date },
valid: { type: Boolean }
}
}]
})
export default LoginToken;

24
src/models/mail.ts Normal file
View File

@ -0,0 +1,24 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IMail extends ModelDataBase {
mail: string;
verified: boolean;
primary: boolean;
}
const Mail = DB.addModel<IMail>({
name: "mail",
versions: [{
migration: () => { },
schema: {
mail: { type: String },
verified: { type: Boolean, default: false },
primary: { type: Boolean }
}
}]
})
export default Mail;

24
src/models/permissions.ts Normal file
View File

@ -0,0 +1,24 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IPermission extends ModelDataBase {
name: string;
description: string;
client: ObjectID;
}
const Permission = DB.addModel<IPermission>({
name: "permission",
versions: [{
migration: () => { },
schema: {
name: { type: String },
description: { type: String },
client: { type: ObjectID }
}
}]
})
export default Permission;

View File

@ -0,0 +1,30 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IRefreshToken extends ModelDataBase {
token: string;
user: ObjectID;
client: ObjectID;
permissions: ObjectID[];
validTill: Date;
valid: boolean;
}
const RefreshToken = DB.addModel<IRefreshToken>({
name: "refresh_token",
versions: [{
migration: () => { },
schema: {
token: { type: String },
user: { type: ObjectID },
client: { type: ObjectID },
permissions: { type: Array },
validTill: { type: Date },
valid: { type: Boolean }
}
}]
})
export default RefreshToken;

55
src/models/regcodes.ts Normal file
View File

@ -0,0 +1,55 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IRegCode extends ModelDataBase {
token: string;
valid: boolean;
validTill: Date;
}
const RegCode = DB.addModel<IRegCode>({
name: "reg_code",
versions: [{
migration: () => { },
schema: {
token: { type: String },
valid: { type: Boolean },
validTill: { type: Date }
}
}]
})
export default RegCode;
// import { Model, Table, Column, ForeignKey, BelongsTo, Unique, CreatedAt, UpdatedAt, DeletedAt, HasMany, BelongsToMany, Default, DataType } from "sequelize-typescript"
// import User from "./user";
// import Permission from "./permissions";
// import RefreshPermission from "./refresh_permission";
// @Table
// export default class RegCode extends Model<RegCode> {
// @Unique
// @Default(DataType.UUIDV4)
// @Column(DataType.UUID)
// token: string
// @Column
// validTill: Date
// @Column
// valid: boolean
// @Column
// @CreatedAt
// creationDate: Date;
// @Column
// @UpdatedAt
// updatedOn: Date;
// @Column
// @DeletedAt
// deletionDate: Date;
// }

69
src/models/user.ts Normal file
View File

@ -0,0 +1,69 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export enum Gender {
none,
male,
female,
other
}
export enum TokenTypes {
OTC,
BACKUP_CODE
}
export interface IUser extends ModelDataBase {
uid: string;
username: string;
name: string;
birthday?: Date
gender: Gender;
admin: boolean;
password: string;
salt: string;
mails: ObjectID[];
phones: { phone: string, verified: boolean, primary: boolean }[];
twofactor: { token: string, valid: boolean, type: TokenTypes }[];
}
const User = DB.addModel<IUser>({
name: "user",
versions: [{
migration: () => { },
schema: {
uid: { type: String, default: () => v4() },
username: { type: String },
name: { type: String },
birthday: { type: Date, optional: true },
gender: { type: Number },
admin: { type: Boolean },
password: { type: String },
salt: { type: String },
mails: { type: Array, default: [] },
phones: {
array: true,
model: true,
type: {
phone: { type: String },
verified: { type: Boolean },
primary: { type: Boolean }
}
},
twofactor: {
array: true,
model: true,
type: {
token: { type: String },
valid: { type: Boolean },
type: { type: Number }
}
}
}
}]
})
export default User;

64
src/testdata.ts Normal file
View File

@ -0,0 +1,64 @@
import User, { Gender } from "./models/user";
import Client from "./models/client";
import { Logging } from "@hibas123/nodelogging";
import RegCode from "./models/regcodes";
import * as moment from "moment";
import Permission from "./models/permissions";
import { ObjectID } from "bson";
import DB from "./database";
export default async function TestData() {
await DB.db.dropDatabase();
let u = await User.findOne({ username: "test" });
if (!u) {
Logging.log("Adding test user");
u = User.new({
username: "test",
birthday: new Date(),
gender: Gender.male,
name: "Test Test",
password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
salt: "test",
admin: true
})
await User.save(u);
}
let c = await Client.findOne({ client_id: "test001" });
if (!c) {
Logging.log("Adding test client")
c = Client.new({
client_id: "test001",
client_secret: "test001",
internal: true,
maintainer: u._id,
name: "Test Client",
website: "http://example.com",
redirect_url: "http://example.com"
})
await Client.save(c);
}
let perm = await Permission.findOne({ where: { id: 0 } });
if (!perm) {
Logging.log("Adding test permission")
perm = Permission.new({
_id: new ObjectID("507f1f77bcf86cd799439011"),
name: "TestPerm",
description: "Permission just for testing purposes",
client: c._id
})
Permission.save(perm);
}
let r = await RegCode.findOne({ where: {} });
if (!r) {
Logging.log("Adding test reg_code")
r = RegCode.new({
token: "test",
valid: true,
validTill: moment().add("1", "year").toDate()
})
await RegCode.save(r);
}
}

21
src/views/admin.ts Normal file
View File

@ -0,0 +1,21 @@
import * as handlebars from "handlebars"
import { readFileSync } from "fs";
import { __ as i__ } from "i18n"
import config from "../config";
let template: handlebars.TemplateDelegate<any>;
function loadStatic() {
let html = readFileSync("./views/out/admin/admin.html").toString();
template = handlebars.compile(html);
}
export default function GetAdminPage(__: typeof i__): string {
if (config.dev) {
loadStatic()
}
let data = {}
return template(data, { helpers: { "i18n": __ } })
}
loadStatic()

26
src/views/authorize.ts Normal file
View File

@ -0,0 +1,26 @@
import { compile, TemplateDelegate } from "handlebars"
import { readFileSync } from "fs";
import { __ as i__ } from "i18n"
import config from "../config";
let template: TemplateDelegate<any>;
function loadStatic() {
let html = readFileSync("./views/out/authorize/authorize.html").toString();
template = compile(html)
}
export default function GetAuthPage(__: typeof i__, appname: string, scopes: { name: string, description: string, logo: string }[]): string {
if (config.dev) {
loadStatic()
}
return template({
title: __("Authorize %s", appname),
information: __("By clicking on ALLOW, you allow this app to access the requested recources."),
scopes: scopes,
// request: request
}, { helpers: { "i18n": __ } });
}
loadStatic()

39
src/views/login.ts Normal file
View File

@ -0,0 +1,39 @@
import * as handlebars from "handlebars"
import { readFileSync } from "fs";
import { __ as i__ } from "i18n"
import config from "../config";
let template: handlebars.TemplateDelegate<any>;
function loadStatic() {
let html = readFileSync("./views/out/login/login.html").toString();
template = handlebars.compile(html);
}
/**
* Benchmarks (5000, 500 cuncurrent)
* Plain:
* - dev 10sec
* - prod 6sec
* Mustache:
* - dev : 15sec
* - prod: 12sec
*
* Handlebars:
* Compile + Render
* - dev 13sec
* - prod 9sec
* Compile on load + Render
* - dev 13sec
* - prod 6sec
*/
export default function GetLoginPage(__: typeof i__): string {
if (config.dev) {
loadStatic()
}
let data = {}
return template(data, { helpers: { "i18n": __ } });
}
loadStatic()

21
src/views/register.ts Normal file
View File

@ -0,0 +1,21 @@
import * as handlebars from "handlebars"
import { readFileSync } from "fs";
import { __ as i__ } from "i18n"
import config from "../config";
let template: handlebars.TemplateDelegate<any>;
function loadStatic() {
let html = readFileSync("./views/out/register/register.html").toString();
template = handlebars.compile(html);
}
export default function GetRegistrationPage(__: typeof i__): string {
if (config.dev) {
loadStatic()
}
let data = {}
return template(data, { helpers: { "i18n": __ } })
}
loadStatic()

104
src/views/views.ts Normal file
View File

@ -0,0 +1,104 @@
import { Router, IRouter, Request } from "express"
import GetLoginPage from "./login";
import GetAuthPage from "./authorize";
import promiseMiddleware from "../helper/promiseMiddleware";
import config from "../config";
import * as Handlebars from "handlebars";
import GetRegistrationPage from "./register";
import GetAdminPage from "./admin";
import { HttpStatusCode } from "../helper/request_error";
import * as moment from "moment";
import Permission, { IPermission } from "../models/permissions";
import Client from "../models/client";
import { Logging } from "@hibas123/nodelogging";
import Stacker from "../api/middlewares/stacker";
import { UserMiddleware, GetUserMiddleware } from "../api/middlewares/user";
Handlebars.registerHelper("appname", () => config.core.name);
const cacheTime = moment.duration(1, "month").asSeconds();
const ViewRouter: IRouter<void> = Router();
ViewRouter.get("/", promiseMiddleware(UserMiddleware), (req, res) => {
res.send("This is the main page")
})
ViewRouter.get("/register", (req, res) => {
res.setHeader("Cache-Control", "public, max-age=" + cacheTime);
res.send(GetRegistrationPage(req.__));
})
ViewRouter.get("/login", (req, res) => {
res.setHeader("Cache-Control", "public, max-age=" + cacheTime);
res.send(GetLoginPage(req.__))
})
ViewRouter.get("/admin", GetUserMiddleware(false, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
}, (req, res) => {
res.send(GetAdminPage(req.__))
})
ViewRouter.get("/auth", Stacker(GetUserMiddleware(false, true), async (req, res) => {
let { scope, redirect_uri, state, client_id }: { [key: string]: string } = req.query;
const sendError = (type) => {
res.redirect(redirect_uri += `?error=${type}&state=${state}`);
}
let client = await Client.findOne({ client_id: client_id })
if (!client) {
return sendError("unauthorized_client")
}
let permissions: IPermission[] = [];
let proms: PromiseLike<void>[] = [];
if (scope) {
for (let perm of scope.split(";")) {
proms.push(Permission.findOne({ id: Number(perm) }).then(p => {
if (!p) return Promise.reject(new Error());
permissions.push(p);
}));
}
}
let err = false;
await Promise.all(proms).catch(err => {
err = true;
})
if (err) {
return sendError("invalid_scope")
}
let scopes = await Promise.all(permissions.map(async perm => {
let client = await Client.findById(perm.client);
return {
name: perm.name,
description: perm.description,
logo: client.logo
}
}))
res.send(GetAuthPage(req.__, client.name, scopes));
}));
if (config.dev) {
const logo = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAlwSFlzAAASdAAAEnQB3mYfeAAAAAZiS0dEAP8A/wD/oL2nkwAAAFR0RVh0Y29tbWVudABGaWxlIHNvdXJjZTogaHR0cHM6Ly9jb21tb25zLndpa2ltZWRpYS5vcmcvd2lraS9GaWxlOkdvb2dsZS1mYXZpY29uLTIwMTUucG5nLE0iZQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wOS0wMlQxNzo1ODowOCswMDowMNkAVU4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDktMDJUMTc6NTg6MDgrMDA6MDCoXe3yAAAAR3RFWHRzb2Z0d2FyZQBJbWFnZU1hZ2ljayA2LjcuNy0xMCAyMDE0LTAzLTA2IFExNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9yZ2+foqIAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6aGVpZ2h0ADQwMUEAWp0AAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMzk0OUtQUwAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNDQxMjE2Njg4mYj7RAAAABN0RVh0VGh1bWI6OlNpemUAMTcuN0tCQhK/wrgAAAAzdEVYdFRodW1iOjpVUkkAZmlsZTovLy90bXAvbG9jYWxjb3B5XzFmZGViZjk0YmZkZC0xLnBuZ8EhOmkAAAkUSURBVGhDzZkLcBXVGcf/u/f9yM07gDq06Rh5BZTAtKMVwQGqaFEZmNLWgoBO1RRFausjbR1qKR0rHUDFtwVtqWPHgc4UmBq1NASUiFQGwiskMqZMSW6Tm+Q+knvvvvp9u0uamNxXctO5P9i5u+ec3T3/c77zfd/ZCBqBMUANhaB2+Ok3CMgSYLVC9ORBLC2F6CswW2WPrAmJHa5D9EAtpGMNkC+2AvE4IIrGcRlV1Q/BRqLGXwn7zNlwzF0A54JbqVIw2oyQUQmRms4g8voLiB14H5qiQHS5AJsNsFghCIk7pr+S2kOSoMWi+r2OG+fBc99a2K+tMltlxoiESKdPIvirGvo9ATHPB9gdEAaOfIboXYjHoAaDsF59DXy/+A3s180ya9MjIyGaLKP70QdoBmohFhSRidDoJxn5jKGuaLSe1K4uMrn5KNj6GgSLxaxMTtpCYh/VoXv9/fRgK0AmlMx0Rg0LivbpA1e4fQfss683KxKTlj2EXtiMrgdWQHB76HCPrQiGni+46D30vsDq7yD00hazIjEphfTUPILIGy+S2xyX9jRnDV53Ah0SecAUJDWtnifXIVq7j9ZD4cjWApuIecroT0jzOdwt9T9+eH/8M3hX/dAsTUxCIcFtz6D3zVdgKSzOSITGsYJiiEZeCGTjPKoamwq/hutoVgXycrDbE86wIaIdeY9vgOfuNWZpcoYVEj10AN3V9xjmlO4IUqe1cAiC0wX79XNg/+Zc2KZUQiwuheBwQCPz0Do7IDWfI8dxEPFD/4AW7IFA7lugqH8ZXYSfRPx8IzzLV5qlqRkihIOT/+uTIOQXpBUb+HbukFhUDO9Dj8H17SVmTWqif38Pod9thNrepr+PUf1t8G34LdxLv6dfp8sQIYEHV0I+8U/da6SCRauBThLwE3gpKo+UMGUH4eeeJRUKfL/eCvedy8ya9BkkJHa0EV2rbqc8qCylSWlmelH89l9hLb/aLB05sSOHoHxxAe7lK8ySzBgkJL7/K+jZ5oMa9kJ0Ui6UQIu+HkhE6YdHIZKvzwX6F4EapE6JrSj6ZSMc0wNQQjb2nkNgr6R2B1C6rz5nRDD9QrTmGmh0pUWs8N3bBN/3m2kRkxiFpsUUxJOnBjpQsJkCJC3uXKLftKT95Ovt5Nc5klLPBZcCpdNJplYJNSqSC+X4EIO1YhKKfv9nviWn0GdE7dhrrAddBCNA66PNjy+Ooqc/hb0iSDHCSibVjfxNW802uYXec82/e4CRDUCmqBy1wFd9Bu7bz8M27RuwjL/CrMwtdNOS6ydSh/9FE0L7i2Eh6xNliJN3QJywyixLn4ZmGR80ynDY0ssS0oUzHq8TqF5IGzsWIr1H64Pznn7T+hK0jLSYDNuiPhJEd2bIm/VxbN4bo5dmWQiNb1zW8NmmPDIojeIF/TcWyfBo1EBwUj40AhGM1SLA5RDgzvLhoYPF6AajSQHjbckiOTs29yTzInfgLnPXuno1mhE5bBYngVXby4zzHIPFRKIsJM00nUK6eZJb0OqGQtMiwuI1RjwZrFVqN85zDN6DWmgyRMHGO0AuSaKGvJkWOWde5Bb68iVvaJiWvuNMLISbIU71csgsyQx+GX9YlBUtoyPZ2DJ6PXW/wGXGc8H7NSrUffDwUGMbie299I5ZkBkOirMFlCjnu4W0j5I8SpOop8nEsFk5rTTMov5NgCScuR9q66u0dx4a2S1QybIUPNNdiX3iPBy883mzZuyZWRNCoSexP5Jo1kq8Anav95gzUkZbyy9NCA+EFTJNhoalnbfg1eh0XAg0oqWn1Wgwxnx4ilIiHukEIhiZ+nztRONLjC5ELF5onPW7WA02QcJ5uRCV/mU4K+ejUKBM2OHDw3UbzTZjy866ONx28yIBMQm44RrjC4wuhBEn3EE6FCpQYaUE8Y3IVNzceRsJUuEWKEXhdSJacTrQgj81Udo/hjRdUnH8C0Vfl4ng9cN51vxKo1H/xkqLnIZQNw2gdGpl13zUx8ahWIwNmVpu3t7bgYPLdqHcd5VZml0WboogTvbPOVoiWMRXS0T8odr42tM/I4JnKlq812Fm22J8Gi9BsWWoCIa/rpS6irDgL6vRFukwS7PH2p19CMWSi2DCUeCem/7nnPqFMN3T96MtGiRTkvQYmQiLSJstmxc3vPtdfNJ+wiwdPeveiuJIs6JntclQKOXl9bOgMoGQWYUTsKR8Hnpl2nekwEpiCmnxL9v/MJ468pxZOjJawmdx6/ZjaDjnRL4ruQgmSN1bv2iwJ+hfIwOZ/MdFcFuc+singm8PS72wW2z40Yy7sXrKEjpP4W5MzgY+x5bjO/G31noUuDV4Ox6Eq2M5NAv1lJzMcHDEd1AQrH1y8KeoYYUc85/CXXurMc5doq+JdFBo3xmRe3VhVWXTMOeKWZhRMgnjXCVwWR2IqRK6oj16HOLnf9T2Gf4d9sNjc8PJwuk1mqUHtvBseC6yi6duiYP/LsLP9gc17HnEjYoJgwd5WCHMK43vYNPRl/WFnclfqFR6nESdjisS/crUHY36yGHVeI1VsOizZxNtlLVy1B74bGol9kFQ3PC0PgtrrJyuI7pIJhDWcO9cG9be4jAKBpBQCLOh4XnsPLMHJc7CjMQMZODj030GpYw0O2G429bBEVhM570IRVVUlYt4ec3wH9eTCmGe/mQ7Xj/1LsoynJnRolFgZlOzB2+C8vkTqBivYdfaxN8MUgphdpzejacattHMFOne6v8Fd61TuoQ5xXfgrdueMEuHJy0hzMmOJvzg/Z8iJsfhpQU61rPD66sz2oWHZqzAY7PuM0sTk7aQyzx+eDPeplwr354HB3mbbAtSKN/riYd1U35t/kZUFleYNcnJWAhzMdyGmo+34MDFBuTZPOQ+HRRzBsXWjOAu8AyE4hHk2d14tGoNVk6+y6xNjxEJuYy/txPbT+7CnpYPaBRDFERdlLFaaR1ZKWXgvcTQ2eLX8T+Ftgzc+ZgSpw2SpMeeNVOXYnH5zWbLzBiVkIGc6jyP2tbD+LjtOM51XdDNQ6POshgjjrAI45pn8CrveFSVTsGcK2fjWxNv1M10NGRNyHAEKJKzuXCA5FliJ5HvyNP3NdkF+C8dn/ikO2g+hwAAAABJRU5ErkJggg=="
ViewRouter.get("/devauth", (req, res) => {
res.send(GetAuthPage(req.__, "Test 05265", [
{
name: "Access Profile",
description: "It allows the application to know who you are. Required for all applications.",
logo: logo
},
{
name: "Test 1",
description: "This is not an real permission. This is used just to verify the layout",
logo: logo
},
{
name: "Test 2",
description: "This is not an real permission. This is used just to verify the layout",
logo: logo
}
]))
})
}
export default ViewRouter;

103
src/web.ts Normal file
View File

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

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"strict": false, /* Enable all strict type-checking options. */
"preserveWatchOutput": true,
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": [
"node_modules/"
],
"files": [
"src/express.d.ts"
],
"include": [
"./src"
]
}

160
views/build.js Normal file
View File

@ -0,0 +1,160 @@
const {
lstatSync,
readdirSync,
mkdirSync,
copyFileSync,
writeFileSync,
readFileSync
} = require('fs')
const {
join,
basename
} = require('path')
const includepaths = require("rollup-plugin-includepaths")
const isDirectory = source => lstatSync(source).isDirectory()
const getDirectories = source =>
readdirSync(source).map(name => join(source, name)).filter(isDirectory)
function ensureDir(folder) {
try {
if (!isDirectory(folder)) mkdirSync(folder)
} catch (e) {
mkdirSync(folder)
}
}
ensureDir("./out")
const sass = require('sass');
function findHead(elm) {
if (elm.tagName === "head") return elm;
for (let i = 0; i < elm.childNodes.length; i++) {
let res = findHead(elm.childNodes[i])
if (res) return res;
}
return undefined;
}
const rollup = require("rollup")
const minify = require("html-minifier").minify
const gzipSize = require('gzip-size');
async function buildPage(folder, name) {
const pagename = basename(folder);
const outpath = "./out/" + pagename;
ensureDir(outpath)
let bundle = await rollup.rollup({
input: `${folder}/${pagename}.js`,
plugins: [includepaths({
paths: ["shared"]
})],
})
let {
code,
map
} = await bundle.generate({
format: "iife",
})
let sass_res = sass.renderSync({
file: folder + `/${pagename}.scss`,
includePaths: ["./node_modules", folder, "./shared"],
outputStyle: "compressed"
})
let css = "<style>\n" + sass_res.css.toString("utf8") + "\n</style>\n";
let script = "<script>\n" + code + "\n</script>\n";
let html = readFileSync(`${folder}/${pagename}.hbs`).toString("utf8");
let idx = html.indexOf("</head>")
if (idx < 0) throw new Error("No head element found")
let idx2 = html.indexOf("</body>")
if (idx2 < 0) throw new Error("No body element found")
if (idx < idx2) {
let part1 = html.slice(0, idx)
let part2 = html.slice(idx, idx2);
let part3 = html.slice(idx2, html.length);
html = part1 + css + part2 + script + part3;
} else {
let part1 = html.slice(0, idx2)
let part2 = html.slice(idx2, idx);
let part3 = html.slice(idx, html.length);
html = part1 + script + part2 + css + part3;
}
let result = minify(html, {
removeAttributeQuotes: true,
collapseWhitespace: true,
html5: true,
keepClosingSlash: true,
minifyCSS: true,
minifyJS: true,
removeComments: true,
useShortDoctype: true
})
let gzips = await gzipSize(result)
writeFileSync(`${outpath}/${pagename}.html`, result)
let stats = {
sass: sass_res.stats,
js: {
chars: code.length
},
css: {
chars: css.length
},
bundle_size: result.length,
gzip_size: gzips
}
writeFileSync(outpath + `/stats.json`, JSON.stringify(stats, null, " "))
}
async function run() {
const pages = getDirectories("./src");
const ProgressBar = require('progress');
// const bar = new ProgressBar('[:bar] :current/:total :percent :elapseds :etas', {
// // schema: '[:bar] :current/:total :percent :elapseds :etas',
// total: pages.length
// });
await Promise.all(pages.map(async e => {
try {
await buildPage(e)
} catch (er) {
console.error("Failed compiling", basename(e))
console.log(er.message)
}
// bar.tick()
}))
console.log("Finished compiling!")
}
if (process.argv.join(" ").toLowerCase().indexOf("watch") < 0) {
run()
} else {
const nodemon = require('nodemon');
nodemon({
script: "dummy.js",
ext: 'js hbs scss',
ignore: ["out/"]
});
nodemon.on('start', function () {
run()
}).on('quit', function () {
process.exit();
}).on('restart', function (files) {
// console.log('App restarted due to: ', files);
});
}

0
views/dummy.js Normal file
View File

3240
views/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
views/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "open_auth_service_views",
"version": "1.0.0",
"main": "index.js",
"author": "Fabian Stamm <dev@fabianstamm.de>",
"license": "MIT",
"scripts": {
"build": "node build.js",
"watch": "node build.js watch"
},
"dependencies": {
"@material/button": "^0.41.0",
"@material/form-field": "^0.41.0",
"@material/radio": "^0.41.0",
"ascii-progress": "^1.0.5",
"html-minifier": "^3.5.21",
"jsdom": "^13.0.0",
"nodemon": "^1.18.6",
"progress": "^2.0.1",
"rollup": "^0.67.0",
"rollup-plugin-includepaths": "^0.2.3",
"sass": "^1.14.3"
},
"devDependencies": {
"gzip-size": "^5.0.0"
}
}

20
views/shared/cookie.js Normal file
View File

@ -0,0 +1,20 @@
export function setCookie(cname, cvalue, exdate) {
var expires = exdate ? `expires=${exdate};` : "";
document.cookie = `${cname}=${cvalue};${expires}path=/`
}
export function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

13
views/shared/event.js Normal file
View File

@ -0,0 +1,13 @@
export default function fireEvent(element, event) {
if (document.createEventObject) {
// dispatch for IE
var evt = document.createEventObject();
return element.fireEvent('on' + event, evt)
}
else {
// dispatch for firefox + others
var evt = document.createEvent("HTMLEvents");
evt.initEvent(event, true, true); // event type,bubbling,cancelable
return !element.dispatchEvent(evt);
}
}

14
views/shared/formdata.js Normal file
View File

@ -0,0 +1,14 @@
export default function getFormData(element) {
let data = {};
if (element.name !== undefined && element.name !== null && element.name !== "") {
if (typeof element.name === "string") {
if (element.type === "checkbox") data[element.name] = element.checked;
else data[element.name] = element.value;
}
}
element.childNodes.forEach(child => {
let res = getFormData(child);
data = Object.assign(data, res);
})
return data;
}

13
views/shared/inputs.js Normal file
View File

@ -0,0 +1,13 @@
document.querySelectorAll(".floating>input").forEach(e => {
function checkState() {
if (e.value !== "") {
if (e.classList.contains("used")) return;
e.classList.add("used")
} else {
if (e.classList.contains("used")) e.classList.remove("used")
}
}
e.addEventListener("change", () => checkState())
checkState()
})

113
views/shared/inputs.scss Normal file
View File

@ -0,0 +1,113 @@
@import "style";
.group {
position: relative;
margin-bottom: 24px;
min-height: 45px;
}
.floating>input {
font-size: 18px;
padding: 10px 10px 10px 5px;
appearance: none;
-webkit-appearance: none;
display: block;
background: #fafafa;
color: #636363;
width: 100%;
border: none;
border-radius: 0;
border-bottom: 1px solid #757575;
}
.floating>input:focus {
outline: none;
}
/* Label */
.floating>label {
color: #999;
font-size: 18px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 5px;
top: 10px;
transition: all 0.2s ease;
}
/* active */
.floating>input:focus~label,
.floating>input.used~label {
top: -.75em;
transform: scale(.75);
left: -2px;
/* font-size: 14px; */
color: $primary;
transform-origin: left;
}
/* Underline */
.bar {
position: relative;
display: block;
width: 100%;
}
.bar:before,
.bar:after {
content: '';
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: $primary;
transition: all 0.2s ease;
}
.bar:before {
left: 50%;
}
.bar:after {
right: 50%;
}
/* active */
.floating>input:focus~.bar:before,
.floating>input:focus~.bar:after {
width: 50%;
}
/* Highlight */
.highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}
/* active */
.floating>input:focus~.highlight {
animation: inputHighlighter 0.3s ease;
}
/* Animations */
@keyframes inputHighlighter {
from {
background: $primary;
}
to {
width: 0;
background: transparent;
}
}

2
views/shared/mat_bs.scss Normal file
View File

@ -0,0 +1,2 @@
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons");
@import url("https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css");

16
views/shared/request.js Normal file
View File

@ -0,0 +1,16 @@
export default function request(endpoint, method, data) {
var headers = new Headers();
headers.set('Content-Type', 'application/json');
return fetch(endpoint, {
method: method,
body: JSON.stringify(data),
headers: headers,
credentials: "include"
}).then(async e => {
if (e.status !== 200) throw new Error(await e.text() || e.statusText);
return e.json()
}).then(e => {
if (e.error) return Promise.reject(new Error(typeof e.error === "string" ? e.error : JSON.stringify(e.error)));
return e;
})
}

1
views/shared/sha512.js Normal file

File diff suppressed because one or more lines are too long

7
views/shared/style.scss Normal file
View File

@ -0,0 +1,7 @@
// $primary: #4a89dc;
$primary: #1E88E5;
$error: #ff2f00;
.btn-primary {
color: white !important;
background-color: $primary !important;
}

244
views/src/admin/admin.hbs Normal file
View File

@ -0,0 +1,244 @@
<html>
<head>
<title>{{i18n "Administration"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js" type="text/javascript"></script>
<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js" type="text/javascript"></script>
<script>
$(document).ready(() => $('body').bootstrapMaterialDesign())
</script>
</head>
<body>
<header class=bg-primary style="display: flex; justify-content: space-between;">
<h3 style="display: inline">{{appname}} {{i18n "Administration"}}
<span id="sitename">LOADING</span>
</h3>
<ul class="nav nav-tabs" style="display: inline-block; margin-left: auto; margin-top: -8px;">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true"
aria-expanded="false">Model</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="?type=user">User</a>
<a class="dropdown-item" href="?type=regcode">RegCode</a>
<a class="dropdown-item" href="?type=client">Client</a>
</div>
</li>
</ul>
</header>
<div id=content>
<div class=container>
<div id=error_cont class=row style="margin-bottom: 24px;display: none;">
<div class=col-sm>
<div class="card error_card">
<div id="error_msg" class="card-body"></div>
</div>
</div>
</div>
<div id=custom_data_cont class=row style="margin-bottom: 24px; display: none">
<div class=col-sm>
<div class=card>
<div id=custom_data class="card-body"></div>
</div>
</div>
</div>
<div class=row>
<div class=col-sm>
<div class=card>
<div class=card-body>
<div id=table-body>
<div style="width: 65px; height: 65px; margin: 0 auto;">
<svg class="spinner" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="circle" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33"
r="30"></circle>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script id="template-spinner" type="text/x-handlebars-template">
<div style="width: 65px; height: 65px; margin: 0 auto;">
<svg class="spinner" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="circle" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</script>
<script id="template-user-list" type="text/x-handlebars-template">
<table class="table table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Name</th>
<th scope="col">Gender</th>
<th scope="col">Role</th>
<th scope="col" style="width: 2.5rem"></th>
</tr>
</thead>
<tbody>
\{{#users}}
<tr>
<td>\{{ username }}</td>
<td>\{{ name }}</td>
<!-- ToDo: Make helper to resolve number to human readaby text-->
<td>\{{humangender gender}}</td>
<td onclick="userOnChangeType('\{{_id}}')">
\{{#if admin}}
<span class="badge badge-danger">Admin</span>
\{{else}}
<span class="badge badge-success">User</span>
\{{/if}}
</td>
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="deleteUser('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
delete
</i>
</button>
</td>
</tr>
\{{/users}}
</tbody>
</table>
</script>
<script id="template-regcode-list" type="text/x-handlebars-template">
<button class="btn btn-raised btn-primary" onclick="createRegcode()">Create</button>
<table class="table table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th scope="col">Code</th>
<th scope="col">Valid</th>
<th scope="col">Till</th>
<th scope="col" style="width: 2.5rem"></th>
</tr>
</thead>
<tbody>
\{{#regcodes}}
<tr>
<td>\{{ token }}</td>
<td>\{{ valid }}</td>
<!-- ToDo: Make helper to resolve number to human readaby text-->
<td>\{{formatDate validTill }}</td>
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="deleteRegcode('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
delete
</i>
</button>
</td>
</tr>
\{{/regcodes}}
</tbody>
</table>
</script>
<script id="template-client-list" type="text/x-handlebars-template">
<button class="btn btn-raised btn-primary" onclick="createClient()">Create</button>
<table class="table table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Secret</th>
<th scope="col">Maintainer</th>
<th scope="col">Name</th>
<th scope="col" style="width: 80px">Type</th>
<th scope="col">Website</th>
<th scope="col" style="width: 2.5rem">
<div></div>
</th>
<th scope="col" style="width: 2.5rem"></th>
</tr>
</thead>
<tbody>
\{{#clients}}
<tr>
<td>\{{ client_id }}</td>
<td>\{{ client_secret }}</td>
<td>\{{ maintainer.username }}</td>
<td>\{{ name }}</td>
<td>
\{{#if internal}}
<span class="badge badge-success">Internal</span>
\{{else}}
<span class="badge badge-danger">External</span>
\{{/if}}
</td>
<td>
<a href="\{{ website }}">\{{ website }}</a>
</td>
<!-- ToDo: Make helper to resolve number to human readaby text-->
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="editClient('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
edit
</i>
</button>
</td>
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="deleteClient('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
delete
</i>
</button>
</td>
</tr>
\{{/clients}}
</tbody>
</table>
</script>
<script id="template-client-form" type="text/x-handlebars-template">
<form class="form" action="JavaScript:void(null)" onsubmit="createClientSubmit(this)" style="margin-bottom: 0">
<input type=hidden value="\{{_id}}" name=id />
<div class="form-group">
<label for="name_input" class="bmd-label-floating">Name</label>
<input type="text" class="form-control" id="name_input" name=name value="\{{name}}">
</div>
<div class="form-group">
<label for="redirect_input" class="bmd-label-floating">Redirect Url</label>
<input type="text" class="form-control" id="redirect_input" name=redirect_url value="\{{redirect_url}}">
</div>
<div class="form-group">
<label for="website_input" class="bmd-label-floating">Website</label>
<input type="text" class="form-control" id="website_input" name=website value="\{{website}}">
</div>
<div class="form-group">
<label for="logo_input" class="bmd-label-floating">Logo</label>
<input type="text" class="form-control" id="logo_input" name=logo value="\{{logo}}">
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="internal_check" \{{#if internal}} checked="checked" \{{/if}} name=internal>
<label class="form-check-label" for="internal_check">Internal</label>
</div>
</div>
<span class="form-group bmd-form-group">
<!-- needed to match padding for floating labels -->
<button type="submit" class="btn btn-raised btn-primary">Save</button>
</span>
</form>
</script>
</body>
</html>

158
views/src/admin/admin.js Normal file
View File

@ -0,0 +1,158 @@
import request from "../../shared/request";
import getFormData from "../../shared/formdata";
Handlebars.registerHelper("humangender", function (value, options) {
switch (value) {
case 1:
return "male";
case 2:
return "female";
case 3:
return "other";
default:
case 0:
return "none";
}
});
// Deprecated since version 0.8.0
Handlebars.registerHelper("formatDate", function (datetime, format) {
return new Date(datetime).toLocaleString();
});
(() => {
const tableb = document.getElementById("table-body");
function setTitle(title) {
document.getElementById("sitename").innerText = title;
}
const cc = document.getElementById("custom_data")
const ccc = document.getElementById("custom_data_cont")
function setCustomCard(content) {
if (!content) {
cc.innerHTML = "";
ccc.style.display = "none";
} else {
cc.innerHTML = content;
ccc.style.display = "";
}
}
const error_cont = document.getElementById("error_cont")
const error_msg = document.getElementById("error_msg")
function catchError(error) {
error_cont.style.display = "";
error_msg.innerText = error.message;
console.log(error);
}
async function renderUser() {
console.log("Rendering User")
setTitle("User")
const listt = Handlebars.compile(document.getElementById("template-user-list").innerText)
async function loadList() {
let data = await request("/api/admin/user", "GET");
tableb.innerHTML = listt({
users: data
})
}
window.userOnChangeType = (id) => {
request("/api/admin/user?id=" + id, "PUT").then(() => loadList()).catch(catchError)
}
window.deleteUser = (id) => {
request("/api/admin/user?id=" + id, "DELETE").then(() => loadList()).catch(catchError)
}
await loadList();
}
async function renderClient() {
console.log("Rendering Client")
setTitle("Client")
const listt = Handlebars.compile(document.getElementById("template-client-list").innerText)
const formt = Handlebars.compile(document.getElementById("template-client-form").innerText)
let clients = [];
async function loadList() {
let data = await request("/api/admin/client", "GET");
clients = data;
tableb.innerHTML = listt({
clients: data
})
}
window.deleteClient = (id) => {
request("/api/admin/client?id=" + id, "DELETE").then(() => loadList()).catch(catchError)
}
window.createClientSubmit = (elm) => {
console.log(elm);
let data = getFormData(elm);
console.log(data);
let id = data.id;
delete data.id;
if (id !== "") {
request("/api/admin/client?id=" + id, "PUT", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError)
} else {
request("/api/admin/client", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError)
}
}
window.createClient = () => {
setCustomCard(formt());
}
window.editClient = (id) => {
let client = clients.find(e => e._id === id);
if (!client) return catchError(new Error("Client does not exist!!"))
setCustomCard(formt(client));
}
await loadList();
}
async function renderRegCode() {
console.log("Rendering RegCode")
setTitle("RegCode")
const listt = Handlebars.compile(document.getElementById("template-regcode-list").innerText)
async function loadList() {
let data = await request("/api/admin/regcode", "GET");
tableb.innerHTML = listt({
regcodes: data
})
}
window.deleteRegcode = (id) => {
request("/api/admin/regcode?id=" + id, "DELETE").then(() => loadList()).catch(catchError)
}
window.createRegcode = () => {
request("/api/admin/regcode", "POST").then(() => loadList()).catch(catchError);
}
await loadList();
}
const type = new URL(window.location.href).searchParams.get("type");
switch (type) {
case "client":
renderClient().catch(catchError)
break
case "regcode":
renderRegCode().catch(catchError)
break;
case "user":
default:
renderUser().catch(catchError);
break;
}
})()

103
views/src/admin/admin.scss Normal file
View File

@ -0,0 +1,103 @@
@import "mat_bs";
@import "style";
.error_card {
color: $error;
padding: 1rem;
font-size: 1rem;
}
.bg-primary {
background-color: $primary !important;
}
.spinner {
-webkit-animation: rotation 1.35s linear infinite;
animation: rotation 1.35s linear infinite;
stroke: $primary;
}
@-webkit-keyframes rotation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
}
@keyframes rotation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
}
.circle {
stroke-dasharray: 180;
stroke-dashoffset: 0;
-webkit-transform-origin: center;
-ms-transform-origin: center;
transform-origin: center;
-webkit-animation: turn 1.35s ease-in-out infinite;
animation: turn 1.35s ease-in-out infinite;
}
@-webkit-keyframes turn {
0% {
stroke-dashoffset: 180;
}
50% {
stroke-dashoffset: 45;
-webkit-transform: rotate(135deg);
transform: rotate(135deg);
}
100% {
stroke-dashoffset: 180;
-webkit-transform: rotate(450deg);
transform: rotate(450deg);
}
}
@keyframes turn {
0% {
stroke-dashoffset: 180;
}
50% {
stroke-dashoffset: 45;
-webkit-transform: rotate(135deg);
transform: rotate(135deg);
}
100% {
stroke-dashoffset: 180;
-webkit-transform: rotate(450deg);
transform: rotate(450deg);
}
}
header {
/* height: 60px; */
margin-bottom: 8px;
padding: 8px 16px;
padding-bottom: 0;
}
table {
word-wrap: break-word;
table-layout: fixed;
}
table td {
vertical-align: inherit !important;
width: auto;
}
.col.form-group {
padding-left: 0 !important;
margin-left: 5px !important;
}

View File

@ -0,0 +1,51 @@
<html>
<head>
<title>{{title}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<div class="card">
<div class="title">
<h1>{{title}}</h1>
</div>
<div class="list">
<hr>
<ul>
{{#scopes}}
<li>
<div style="display:inline-block">
{{#if logo}}
<div class="image">
<img width="50px" height="50px" src="{{logo}}">
</div>
{{/if}}
<div class="text">
<h3 class="scope_title">{{name}}</h3>
<p class="scope_description">{{description}}</p>
</div>
</div>
<hr>
</li>
{{/scopes}}
</ul>
</div>
<div>
{{information}}
</div>
<div>
<div style="text-align:right;">
<button class="mdc-button mdc-button--raised grey-button" id="cancel">Cancel</button>
<button class="mdc-button mdc-button--raised blue-button" id="allow">Allow</button>
</div>
</div>
</div>
<form method="post" action="/api/oauth/auth?" id="hidden_form" style="display: none;"></form>
</body>
</html>

View File

@ -0,0 +1,15 @@
document.getElementById("hidden_form").action += window.location.href.split("?")[1];
function submit() {
document.getElementById("hidden_form").submit();
}
document.getElementById("cancel").onclick = () => {
let u = new URL(window.location);
let uri = u.searchParams.get("redirect_uri");
window.location.href = uri + "?error=access_denied&state=" + u.searchParams.get("state");
}
document.getElementById("allow").onclick = () => {
submit()
}

View File

@ -0,0 +1,72 @@
@import "@material/button/mdc-button";
.blue-button {
background: #4a89dc !important;
}
.grey-button {
background: #797979 !important;
}
hr {
// display: block;
// height: 1px;
border: 0;
border-top: 1px solid #b8b8b8;
// margin: 1em 0;
// padding: 0;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
.title {
text-align:center;
}
h1, h3 { font-weight: 300; }
h1 { color: #636363; }
ul {
list-style: none;
padding-left: 0;
}
.image {
display: block;
height: 50px;
width: 50px;
float: left;
}
.text {
display: block;
width: calc(100% - 60px);
height: 50px;
float: right;
padding-left: 10px;
}
.scope_title {
margin-top: 0;
margin-bottom: 0;
padding-left: 5px;
}
.scope_description {
margin-top: 0;
padding-left: 15px;
font-size: 13px;
color: #202020;
}
.card {
max-width: 480px;
margin: 4em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px;
}

47
views/src/login/login.hbs Normal file
View File

@ -0,0 +1,47 @@
<html>
<head>
<title>{{i18n "Login"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<hgroup>
<h1>{{i18n "Login"}}</h1>
</hgroup>
<form action="JavaScript:void(0)">
<div class="loader_box" id="loader">
<div class="loader"></div>
</div>
<div id="container">
<div class="floating group" id="usernamegroup">
<input type="text" id="username" autofocus>
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Username or Email"}}</label>
<div class="error invisible" id="uerrorfield"></div>
</div>
<div class="floating group invisible" id="passwordgroup">
<input type="password" id="password">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Password"}}</label>
<div class="error invisible" id="perrorfield"></div>
</div>
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}}
</button>
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised invisible">{{i18n "Login"}}
</button>
</div>
</form>
<footer>
<!-- <a href="http://www.polymer-project.org/" target="_blank">
<img src="https://www.polymer-project.org/images/logos/p-logo.svg">
</a> -->
<p>Powered by {{appname}}</p>
</footer>
</body>
</html>

140
views/src/login/login.js Normal file
View File

@ -0,0 +1,140 @@
import sha from "sha512";
import {
setCookie,
getCookie
} from "cookie"
import "inputs"
const loader = document.getElementById("loader")
const container = document.getElementById("container")
const usernameinput = document.getElementById("username")
const usernamegroup = document.getElementById("usernamegroup")
const uerrorfield = document.getElementById("uerrorfield")
const passwordinput = document.getElementById("password")
const passwordgroup = document.getElementById("passwordgroup")
const perrorfield = document.getElementById("perrorfield")
const nextbutton = document.getElementById("nextbutton")
const loginbutton = document.getElementById("loginbutton")
let username;
let salt;
usernameinput.focus()
const loading = () => {
container.style.filter = "blur(2px)";
loader.style.display = "";
}
const loading_fin = () => {
container.style.filter = ""
loader.style.display = "none";
}
loading_fin();
usernameinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) nextbutton.click();
clearError(uerrorfield);
}
nextbutton.onclick = async () => {
loading();
username = usernameinput.value;
try {
let res = await fetch("/api/user/login?type=username&username=" + username, {
method: "POST"
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
salt = res.salt;
usernamegroup.classList.add("invisible")
nextbutton.classList.add("invisible")
passwordgroup.classList.remove("invisible")
loginbutton.classList.remove("invisible")
passwordinput.focus()
} catch (e) {
showError(uerrorfield, e.message)
}
loading_fin()
}
passwordinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) loginbutton.click();
clearError(perrorfield);
}
loginbutton.onclick = async () => {
loading();
let pw = sha(salt + passwordinput.value);
try {
let {
login,
special
} = await fetch("/api/user/login?type=password", {
method: "POST",
body: JSON.stringify({
username: usernameinput.value,
password: pw
}),
headers: {
'content-type': 'application/json'
},
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
setCookie("login", login.token, login.expires)
setCookie("special", special.token, special.expires)
let d = new Date()
d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
setCookie("username", username, d.toUTCString());
let url = new URL(window.location.href);
let state = url.searchParams.get("state")
let red = "/"
if (state) {
let base64 = url.searchParams.get("base64")
if (base64)
red = atob(state)
else
red = state
}
window.location.href = red;
} catch (e) {
passwordinput.value = "";
showError(perrorfield, e.message);
}
loading_fin();
}
function clearError(field) {
field.innerText = "";
field.classList.add("invisible")
}
function showError(field, error) {
field.innerText = error;
field.classList.remove("invisible")
}
username = getCookie("username")
if (username) {
usernameinput.value = username;
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
usernameinput.dispatchEvent(evt);
}

129
views/src/login/login.scss Normal file
View File

@ -0,0 +1,129 @@
@import "@material/button/mdc-button";
@import "inputs";
@import "style";
#loginbutton,
#nextbutton {
width: 100%;
background: $primary; // text-shadow: 1px 1px 0 rgba(39, 110, 204, .5);
}
* {
box-sizing: border-box;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
hgroup {
text-align: center;
margin-top: 4em;
}
h1,
h3 {
font-weight: 300;
}
h1 {
color: #636363;
}
h3 {
color: $primary;
}
form {
max-width: 380px;
margin: 4em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px;
position: relative;
}
.loader_box {
width: 64px;
height: 64px;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
}
.loader{
display: inline-block;
position: relative;
z-index: 100;
}
.loader:after {
content: " ";
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid #000000;
border-color: #000000 transparent #000000 transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
footer {
text-align: center;
}
footer p {
color: #888;
font-size: 13px;
letter-spacing: .4px;
}
footer a {
color: $primary;
text-decoration: none;
transition: all .2s ease;
}
footer a:hover {
color: #666;
text-decoration: underline;
}
footer img {
width: 80px;
transition: all .2s ease;
}
footer img:hover {
opacity: .83;
}
footer img:focus,
footer a:focus {
outline: none;
}
.invisible {
display: none;
}
.errorColor {
background: $error !important;
}
.error {
color: $error;
margin-top: 5px;
font-size: 13px;
}

7
views/src/main/main.hbs Normal file
View File

@ -0,0 +1,7 @@
<html>
<head></head>
<body></body>
</html>

1
views/src/main/main.js Normal file
View File

@ -0,0 +1 @@
console.log("Hello World")

0
views/src/main/main.scss Normal file
View File

View File

@ -0,0 +1,115 @@
<html>
<head>
<title>{{i18n "Register"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<hgroup>
<h1>{{i18n "Register"}}</h1>
</hgroup>
<form action="JavaScript:void(0)">
<div class="error invisible" id="error" style="font-size: 18px; margin-bottom: 16px"></div>
<div class="floating group">
<input type="text" id="regcode">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Registration code"}}</label>
<div class="error invisible" id="err_regcode"></div>
</div>
<div class="floating group">
<input type="text" id="username">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Username"}}</label>
<div class="error invisible" id="err_username"></div>
</div>
<div class="floating group">
<input type="text" id="name">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Name"}}</label>
<div class="error invisible" id="err_name"></div>
</div>
<div class="error invisible" id="err_gender"></div>
<div class="mdc-form-field group">
<div class="mdc-radio">
<input class="mdc-radio__native-control" type="radio" id="radio-male" name="radios" checked>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
<label for="radio-male" style="position: relative;">{{i18n "Male"}}</label>
<div class="mdc-radio">
<input class="mdc-radio__native-control" type="radio" id="radio-female" name="radios" checked>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
<label for="radio-female" style="position: relative;">{{i18n "Female"}}</label>
<div class="mdc-radio">
<input class="mdc-radio__native-control" type="radio" id="radio-other" name="radios" checked>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
<label for="radio-other" style="position: relative;">{{i18n "Other"}}</label>
</div>
<div class="floating group">
<input type="text" id="mail">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Mail"}}</label>
<div class="error invisible" id="err_mail"></div>
</div>
<div class="floating group">
<input type="password" id="password">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Password"}}</label>
<div class="error invisible" id="err_password"></div>
</div>
<div class="floating group">
<input type="password" id="passwordrep">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Repeat Password"}}</label>
<div class="error invisible" id="err_passwordrep"></div>
</div>
<button type="button" id="registerbutton" class="mdc-button mdc-button--raised">{{i18n "Register"}}
</button>
</form>
<footer>
<p>Powered by {{appname}}</p>
</footer>
<script type="json" id="error_codes">
{
"noregcode": "{{i18n "Registration code required"}}",
"nousername": "{{i18n "Username required"}}",
"noname": "{{i18n "Name required"}}",
"nomail": "{{i18n "Mail required"}}",
"nogender": "{{i18n "You need to select one of the options"}}",
"nopassword": "{{i18n "Password is required"}}",
"nomatch": "{{i18n "The passwords do not match"}}"
}
</script>
</body>
</html>

View File

@ -0,0 +1,149 @@
import "inputs";
import sha from "sha512";
import fireEvent from "event"
(() => {
const translations = JSON.parse(document.getElementById("error_codes").innerText)
const regcode = document.getElementById("regcode")
regcode.value = new URL(window.location.href).searchParams.get("regcode")
fireEvent(regcode, "change");
function showError(element, message) {
if (typeof element === "string")
element = document.getElementById(element)
if (!element) console.error("Element not found,", element)
element.innerText = message;
if (!message) {
if (!element.classList.contains("invisible"))
element.classList.add("invisible")
} else {
element.classList.remove("invisible");
}
}
function makeid(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
const username = document.getElementById("username")
const name = document.getElementById("name")
const mail = document.getElementById("mail")
const password = document.getElementById("password")
const passwordrep = document.getElementById("passwordrep")
const radio_male = document.getElementById("radio-male")
const radio_female = document.getElementById("radio-female")
const radio_other = document.getElementById("radio-other")
const registerButton = document.getElementById("registerbutton")
registerButton.onclick = () => {
console.log("Register")
showError("error");
let error = false;
if (!regcode.value) {
showError("err_regcode", translations["noregcode"])
error = true;
} else {
showError("err_regcode")
}
if (!username.value) {
showError("err_username", translations["nousername"])
error = true;
} else {
showError("err_username")
}
if (!name.value) {
showError("err_name", translations["noname"])
error = true;
} else {
showError("err_name")
}
if (!mail.value) {
showError("err_mail", translations["nomail"])
error = true;
} else {
showError("err_mail")
}
if (!password.value) {
showError("err_password", translations["nopassword"])
error = true;
} else {
showError("err_password")
}
if (password.value !== passwordrep.value) {
showError("err_passwordrep", translations["nomatch"])
error = true;
} else {
showError("err_passwordrep")
}
if (error) return;
let gender;
if (radio_male.checked) {
gender = "male"
} else if (radio_female.checked) {
gender = "female"
} else {
gender = "other"
}
let salt = makeid(10)
//username, password, salt, mail, gender, name, birthday, regcode
let body = {
username: username.value,
gender: gender,
mail: mail.value,
name: name.value,
regcode: regcode.value,
salt: salt,
password: sha(salt + password.value)
}
fetch("/api/user/register", {
method: "POST",
body: JSON.stringify(body),
headers: {
'content-type': 'application/json'
},
}).then(async e => {
if (e.status !== 200) return Promise.reject(new Error(await e.text() || e.statusText));
return e.json()
}).then(data => {
if (data.error) {
if (!Array.isArray(data.error)) return Promise.reject(new Error(data.error));
let ce = [];
data.error.forEach(e => {
let ef = document.getElementById("err_" + e.field);
if (!ef) ce.push(e);
else {
showError(ef, e.message);
}
})
if (ce.length > 0) {
showError("error", ce.join("<br>"));
}
} else {
window.location.href = "/login";
}
}).catch(e => {
showError("error", e.message);
})
}
})()

View File

@ -0,0 +1,95 @@
@import "@material/button/mdc-button";
@import "@material/form-field/mdc-form-field";
@import "@material/radio/mdc-radio";
@import "inputs";
* {
box-sizing: border-box;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
hgroup {
text-align: center;
margin-top: 1em;
}
h1,
h3 {
font-weight: 300;
}
h1 {
color: #636363;
}
h3 {
color: $primary;
}
form {
max-width: 380px;
margin: 1em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px;
}
#registerbutton {
width: 100%;
background: $primary;
text-shadow: 1px 1px 0 rgba(39, 110, 204, .5);
}
footer {
text-align: center;
}
footer p {
color: #888;
font-size: 13px;
letter-spacing: .4px;
}
footer a {
color: $primary;
text-decoration: none;
transition: all .2s ease;
}
footer a:hover {
color: #666;
text-decoration: underline;
}
footer img {
width: 80px;
transition: all .2s ease;
}
footer img:hover {
opacity: .83;
}
footer img:focus,
footer a:focus {
outline: none;
}
.invisible {
display: none;
}
.errorColor {
background: $error !important;
}
.error {
color: $error;
margin-top: 5px;
font-size: 13px;
}

14
views/src/user/user.hbs Normal file
View File

@ -0,0 +1,14 @@
<html>
<head>
<title>{{title}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
</body>
</html>

1
views/src/user/user.js Normal file
View File

@ -0,0 +1 @@
console.log("Hello World")

0
views/src/user/user.scss Normal file
View File

2233
views/yarn.lock Normal file

File diff suppressed because it is too large Load Diff