Compare commits
20 Commits
improving-
...
v1.2.1
Author | SHA1 | Date | |
---|---|---|---|
80aace7b72 | |||
1e2bb83447 | |||
0453e461c9 | |||
532107c479 | |||
39628e6175 | |||
e814aacb86 | |||
bb01f7d62d | |||
779e7e1478 | |||
0e55b154ed | |||
7c0d5949ab | |||
05bef4fd49 | |||
d6b72f4fc7 | |||
69aa9cb47e | |||
dd10cae1cd | |||
6b4ad81940 | |||
2c4c87927d | |||
e0ea7275f7 | |||
51a8609880 | |||
77fedd2815 | |||
e51069deab |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
node_modules/
|
||||||
|
Backend/node_modules
|
||||||
|
Backend/keys
|
||||||
|
Backend/logs
|
||||||
|
Backend/lib
|
||||||
|
Backend/doc
|
||||||
|
Backend/config.ini
|
||||||
|
Frontend/build
|
||||||
|
Frontend/node_modules
|
||||||
|
FrontendLegacy/node_modules
|
||||||
|
FrontendLegacy/out
|
10
.drone.yml
10
.drone.yml
@ -6,6 +6,7 @@ steps:
|
|||||||
- name: Build with node
|
- name: Build with node
|
||||||
image: node:12
|
image: node:12
|
||||||
commands:
|
commands:
|
||||||
|
- npm config set registry https://npm.hibas123.de
|
||||||
- npm install
|
- npm install
|
||||||
- npm run install
|
- npm run install
|
||||||
- npm run build
|
- npm run build
|
||||||
@ -17,8 +18,11 @@ steps:
|
|||||||
password:
|
password:
|
||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
repo: hibas123.azurecr.io/authserver
|
repo: docker.hibas123.de/authserver
|
||||||
registry: hibas123.azurecr.io
|
registry: docker.hibas123.de
|
||||||
debug: true
|
debug: true
|
||||||
when:
|
when:
|
||||||
branch: master
|
branch: [master]
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -9,4 +9,7 @@ logs/
|
|||||||
yarn-error\.log
|
yarn-error\.log
|
||||||
config.ini
|
config.ini
|
||||||
.env
|
.env
|
||||||
doc/
|
doc/
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/install-state.gz
|
||||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "views_repo"]
|
|
||||||
path = views_repo
|
|
||||||
url = ../OpenAuth_views
|
|
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
873
.yarn/releases/yarn-3.5.0.cjs
vendored
Normal file
873
.yarn/releases/yarn-3.5.0.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
9
.yarnrc.yml
Normal file
9
.yarnrc.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
npmRegistryServer: "https://npm.hibas123.de"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-3.5.0.cjs
|
6
Backend/apidoc.json
Normal file
6
Backend/apidoc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "openauth",
|
||||||
|
"description": "Open Auth REST API",
|
||||||
|
"title": "Open Auth REST",
|
||||||
|
"url": "/api"
|
||||||
|
}
|
@ -38,5 +38,6 @@
|
|||||||
"Login token invalid": "Login token invalid",
|
"Login token invalid": "Login token invalid",
|
||||||
"No login token": "No login token",
|
"No login token": "No login token",
|
||||||
"You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)",
|
"You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)",
|
||||||
"You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)"
|
"You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)",
|
||||||
|
"Special token invalid": "Special token invalid"
|
||||||
}
|
}
|
17
Backend/locales/en.json
Normal file
17
Backend/locales/en.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Login": "Login",
|
||||||
|
"Username or Email": "Username or Email",
|
||||||
|
"Password": "Password",
|
||||||
|
"Next": "Next",
|
||||||
|
"Invalid code": "Invalid code",
|
||||||
|
"You are not logged in or your login is expired": "You are not logged in or your login is expired",
|
||||||
|
"User not found": "User not found",
|
||||||
|
"No special token": "No special token",
|
||||||
|
"You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)",
|
||||||
|
"Special token invalid": "Special token invalid",
|
||||||
|
"You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)",
|
||||||
|
"No login token": "No login token",
|
||||||
|
"Login token invalid": "Login token invalid",
|
||||||
|
"Authorize %s": "Authorize %s",
|
||||||
|
"By clicking on ALLOW, you allow this app to access the requested recources.": "By clicking on ALLOW, you allow this app to access the requested recources."
|
||||||
|
}
|
71
Backend/package.json
Normal file
71
Backend/package.json
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "@hibas123/openauth-backend",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"author": "Fabian Stamm <dev@fabianstamm.de>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "run-s build-ts build-doc",
|
||||||
|
"build-doc": "apidoc -i src/ -p apidoc/",
|
||||||
|
"build-ts": "tsc",
|
||||||
|
"start": "node lib/index.js",
|
||||||
|
"dev": "nodemon -e ts --exec ts-node src/index.ts",
|
||||||
|
"format": "prettier --write \"src/**\""
|
||||||
|
},
|
||||||
|
"pipelines": {
|
||||||
|
"install": [
|
||||||
|
"cd views && npm install",
|
||||||
|
"git submodule init",
|
||||||
|
"git submodule update",
|
||||||
|
"cd views_repo && npm install"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/body-parser": "^1.19.2",
|
||||||
|
"@types/compression": "^1.7.2",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
|
"@types/dotenv": "^8.2.0",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/i18n": "^0.13.6",
|
||||||
|
"@types/ini": "^1.3.31",
|
||||||
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
|
"@types/mongodb": "^3.6.20",
|
||||||
|
"@types/node": "^18.15.11",
|
||||||
|
"@types/node-rsa": "^1.1.1",
|
||||||
|
"@types/qrcode": "^1.5.0",
|
||||||
|
"@types/speakeasy": "^2.0.7",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
|
"apidoc": "^0.54.0",
|
||||||
|
"concurrently": "^8.0.1",
|
||||||
|
"nodemon": "^2.0.22",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hibas123/config": "^1.1.2",
|
||||||
|
"@hibas123/nodelogging": "^3.1.3",
|
||||||
|
"@hibas123/nodeloggingserver_client": "^1.1.2",
|
||||||
|
"@hibas123/openauth-views-v1": "workspace:^",
|
||||||
|
"@hibas123/safe_mongo": "^1.7.1",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
|
"i18n": "^0.15.1",
|
||||||
|
"ini": "^4.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mongodb": "^3.7.3",
|
||||||
|
"node-rsa": "^1.1.1",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"qrcode": "^1.5.1",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"speakeasy": "^2.0.0",
|
||||||
|
"u2f": "^0.1.3",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.5.0"
|
||||||
|
}
|
@ -5,16 +5,15 @@ import Client from "../../models/client";
|
|||||||
import verify, { Types } from "../middlewares/verify";
|
import verify, { Types } from "../middlewares/verify";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
|
|
||||||
|
|
||||||
const ClientRouter: Router = Router();
|
const ClientRouter: Router = Router();
|
||||||
ClientRouter.route("/")
|
ClientRouter.route("/")
|
||||||
/**
|
/**
|
||||||
* @api {get} /admin/client
|
* @api {get} /admin/client
|
||||||
* @apiName AdminGetClients
|
* @apiName AdminGetClients
|
||||||
*
|
*
|
||||||
* @apiGroup admin_client
|
* @apiGroup admin_client
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object[]} clients
|
* @apiSuccess {Object[]} clients
|
||||||
* @apiSuccess {String} clients._id The internally used id
|
* @apiSuccess {String} clients._id The internally used id
|
||||||
* @apiSuccess {String} clients.maintainer
|
* @apiSuccess {String} clients.maintainer
|
||||||
@ -26,24 +25,26 @@ ClientRouter.route("/")
|
|||||||
* @apiSuccess {String} clients.client_id Client ID used outside of DB
|
* @apiSuccess {String} clients.client_id Client ID used outside of DB
|
||||||
* @apiSuccess {String} clients.client_secret
|
* @apiSuccess {String} clients.client_secret
|
||||||
*/
|
*/
|
||||||
.get(promiseMiddleware(async (req, res) => {
|
.get(
|
||||||
let clients = await Client.find({});
|
promiseMiddleware(async (req, res) => {
|
||||||
//ToDo check if user is required!
|
let clients = await Client.find({});
|
||||||
res.json(clients);
|
//ToDo check if user is required!
|
||||||
}))
|
res.json(clients);
|
||||||
|
})
|
||||||
|
)
|
||||||
/**
|
/**
|
||||||
* @api {get} /admin/client
|
* @api {get} /admin/client
|
||||||
* @apiName AdminAddClients
|
* @apiName AdminAddClients
|
||||||
*
|
*
|
||||||
* @apiGroup admin_client
|
* @apiGroup admin_client
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiParam {Boolean} internal Is it an internal app
|
* @apiParam {Boolean} internal Is it an internal app
|
||||||
* @apiParam {String} name
|
* @apiParam {String} name
|
||||||
* @apiParam {String} redirect_url
|
* @apiParam {String} redirect_url
|
||||||
* @apiParam {String} website
|
* @apiParam {String} website
|
||||||
* @apiParam {String} logo
|
* @apiParam {String} logo
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object[]} clients
|
* @apiSuccess {Object[]} clients
|
||||||
* @apiSuccess {String} clients._id The internally used id
|
* @apiSuccess {String} clients._id The internally used id
|
||||||
* @apiSuccess {String} clients.maintainer
|
* @apiSuccess {String} clients.maintainer
|
||||||
@ -55,62 +56,78 @@ ClientRouter.route("/")
|
|||||||
* @apiSuccess {String} clients.client_id Client ID used outside of DB
|
* @apiSuccess {String} clients.client_id Client ID used outside of DB
|
||||||
* @apiSuccess {String} clients.client_secret
|
* @apiSuccess {String} clients.client_secret
|
||||||
*/
|
*/
|
||||||
.post(verify({
|
.post(
|
||||||
internal: {
|
verify(
|
||||||
type: Types.BOOLEAN,
|
{
|
||||||
optional: true
|
internal: {
|
||||||
},
|
type: Types.BOOLEAN,
|
||||||
name: {
|
optional: true,
|
||||||
type: Types.STRING
|
},
|
||||||
},
|
name: {
|
||||||
redirect_url: {
|
type: Types.STRING,
|
||||||
type: Types.STRING
|
},
|
||||||
},
|
redirect_url: {
|
||||||
website: {
|
type: Types.STRING,
|
||||||
type: Types.STRING
|
},
|
||||||
},
|
website: {
|
||||||
logo: {
|
type: Types.STRING,
|
||||||
type: Types.STRING,
|
},
|
||||||
optional: true
|
logo: {
|
||||||
}
|
type: Types.STRING,
|
||||||
}, true), promiseMiddleware(async (req, res) => {
|
optional: true,
|
||||||
req.body.client_secret = randomBytes(32).toString("hex");
|
},
|
||||||
let client = Client.new(req.body);
|
featured: {
|
||||||
client.maintainer = req.user._id;
|
type: Types.BOOLEAN,
|
||||||
await Client.save(client)
|
optional: true,
|
||||||
res.json(client);
|
},
|
||||||
}))
|
description: {
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
ClientRouter.route("/:id")
|
ClientRouter.route("/:id")
|
||||||
/**
|
/**
|
||||||
* @api {delete} /admin/client/:id
|
* @api {delete} /admin/client/:id
|
||||||
* @apiParam {String} id Client _id
|
* @apiParam {String} id Client _id
|
||||||
* @apiName AdminDeleteClient
|
* @apiName AdminDeleteClient
|
||||||
*
|
*
|
||||||
* @apiGroup admin_client
|
* @apiGroup admin_client
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {Boolean} success
|
* @apiSuccess {Boolean} success
|
||||||
*/
|
*/
|
||||||
.delete(promiseMiddleware(async (req, res) => {
|
.delete(
|
||||||
let { id } = req.params;
|
promiseMiddleware(async (req, res) => {
|
||||||
await Client.delete(id);
|
let { id } = req.params;
|
||||||
res.json({ success: true });
|
await Client.delete(id);
|
||||||
}))
|
res.json({ success: true });
|
||||||
|
})
|
||||||
|
)
|
||||||
/**
|
/**
|
||||||
* @api {put} /admin/client/:id
|
* @api {put} /admin/client/:id
|
||||||
* @apiParam {String} id Client _id
|
* @apiParam {String} id Client _id
|
||||||
* @apiName AdminUpdateClient
|
* @apiName AdminUpdateClient
|
||||||
*
|
*
|
||||||
* @apiGroup admin_client
|
* @apiGroup admin_client
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiParam {Boolean} internal Is it an internal app
|
* @apiParam {Boolean} internal Is it an internal app
|
||||||
* @apiParam {String} name
|
* @apiParam {String} name
|
||||||
* @apiParam {String} redirect_url
|
* @apiParam {String} redirect_url
|
||||||
* @apiParam {String} website
|
* @apiParam {String} website
|
||||||
* @apiParam {String} logo
|
* @apiParam {String} logo
|
||||||
*
|
*
|
||||||
* @apiSuccess {String} _id The internally used id
|
* @apiSuccess {String} _id The internally used id
|
||||||
* @apiSuccess {String} maintainer UserID of client maintainer
|
* @apiSuccess {String} maintainer UserID of client maintainer
|
||||||
* @apiSuccess {Boolean} internal Defines if it is a internal client
|
* @apiSuccess {Boolean} internal Defines if it is a internal client
|
||||||
@ -118,40 +135,57 @@ ClientRouter.route("/:id")
|
|||||||
* @apiSuccess {String} redirect_url Redirect URL after login
|
* @apiSuccess {String} redirect_url Redirect URL after login
|
||||||
* @apiSuccess {String} website Website of Client
|
* @apiSuccess {String} website Website of Client
|
||||||
* @apiSuccess {String} logo The Logo of the Client (optional)
|
* @apiSuccess {String} logo The Logo of the Client (optional)
|
||||||
* @apiSuccess {String} client_id Client ID used outside of DB
|
* @apiSuccess {String} client_id Client ID used outside of DB
|
||||||
* @apiSuccess {String} client_secret The client secret, that can be used to obtain token
|
* @apiSuccess {String} client_secret The client secret, that can be used to obtain token
|
||||||
*/
|
*/
|
||||||
.put(verify({
|
.put(
|
||||||
internal: {
|
verify(
|
||||||
type: Types.BOOLEAN,
|
{
|
||||||
optional: true
|
internal: {
|
||||||
},
|
type: Types.BOOLEAN,
|
||||||
name: {
|
optional: true,
|
||||||
type: Types.STRING,
|
},
|
||||||
optional: true
|
name: {
|
||||||
},
|
type: Types.STRING,
|
||||||
redirect_url: {
|
optional: true,
|
||||||
type: Types.STRING,
|
},
|
||||||
optional: true
|
redirect_url: {
|
||||||
},
|
type: Types.STRING,
|
||||||
website: {
|
optional: true,
|
||||||
type: Types.STRING,
|
},
|
||||||
optional: true
|
website: {
|
||||||
},
|
type: Types.STRING,
|
||||||
logo: {
|
optional: true,
|
||||||
type: Types.STRING,
|
},
|
||||||
optional: true
|
logo: {
|
||||||
}
|
type: Types.STRING,
|
||||||
}, true), promiseMiddleware(async (req, res) => {
|
optional: true,
|
||||||
let { id } = req.query;
|
},
|
||||||
let client = await Client.findById(id);
|
featured: {
|
||||||
if (!client) throw new RequestError(req.__("Client not found"), HttpStatusCode.BAD_REQUEST);
|
type: Types.BOOLEAN,
|
||||||
for (let key in req.body) {
|
optional: true,
|
||||||
client[key] = req.body[key];
|
},
|
||||||
}
|
description: {
|
||||||
await Client.save(client);
|
type: Types.STRING,
|
||||||
res.json(client);
|
optional: true,
|
||||||
}))
|
},
|
||||||
|
},
|
||||||
|
true
|
||||||
|
),
|
||||||
|
promiseMiddleware(async (req, res) => {
|
||||||
|
let { id } = req.query as { [key: string]: string };
|
||||||
|
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;
|
||||||
export default ClientRouter;
|
|
@ -9,12 +9,16 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
|||||||
const AdminRoute: Router = Router();
|
const AdminRoute: Router = Router();
|
||||||
|
|
||||||
AdminRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
|
AdminRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
|
||||||
if (!req.isAdmin) throw new RequestError("You have no permission to access this API", HttpStatusCode.FORBIDDEN);
|
if (!req.isAdmin)
|
||||||
else next()
|
throw new RequestError(
|
||||||
|
"You have no permission to access this API",
|
||||||
|
HttpStatusCode.FORBIDDEN
|
||||||
|
);
|
||||||
|
else next();
|
||||||
});
|
});
|
||||||
|
|
||||||
AdminRoute.use("/client", ClientRoute);
|
AdminRoute.use("/client", ClientRoute);
|
||||||
AdminRoute.use("/regcode", RegCodeRoute)
|
AdminRoute.use("/regcode", RegCodeRoute);
|
||||||
AdminRoute.use("/user", UserRoute)
|
AdminRoute.use("/user", UserRoute);
|
||||||
AdminRoute.use("/permission", PermissionRoute);
|
AdminRoute.use("/permission", PermissionRoute);
|
||||||
export default AdminRoute;
|
export default AdminRoute;
|
@ -28,7 +28,7 @@ PermissionRoute.route("/")
|
|||||||
promiseMiddleware(async (req, res) => {
|
promiseMiddleware(async (req, res) => {
|
||||||
let query = {};
|
let query = {};
|
||||||
if (req.query.client) {
|
if (req.query.client) {
|
||||||
query = { client: new ObjectID(req.query.client) };
|
query = { client: new ObjectID(req.query.client as string) };
|
||||||
}
|
}
|
||||||
let permissions = await Permission.find(query);
|
let permissions = await Permission.find(query);
|
||||||
res.json(permissions);
|
res.json(permissions);
|
||||||
@ -56,18 +56,18 @@ PermissionRoute.route("/")
|
|||||||
verify(
|
verify(
|
||||||
{
|
{
|
||||||
client: {
|
client: {
|
||||||
type: Types.STRING
|
type: Types.STRING,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: Types.STRING
|
type: Types.STRING,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: Types.STRING
|
type: Types.STRING,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: Types.ENUM,
|
type: Types.ENUM,
|
||||||
values: ["user", "client"]
|
values: ["user", "client"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
),
|
),
|
||||||
@ -83,7 +83,7 @@ PermissionRoute.route("/")
|
|||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
client: client._id,
|
client: client._id,
|
||||||
grant_type: req.body.type
|
grant_type: req.body.type,
|
||||||
});
|
});
|
||||||
await Permission.save(permission);
|
await Permission.save(permission);
|
||||||
res.json(permission);
|
res.json(permission);
|
||||||
@ -102,7 +102,7 @@ PermissionRoute.route("/")
|
|||||||
*/
|
*/
|
||||||
.delete(
|
.delete(
|
||||||
promiseMiddleware(async (req, res) => {
|
promiseMiddleware(async (req, res) => {
|
||||||
let { id } = req.query;
|
let { id } = req.query as { [key: string]: string };
|
||||||
await Permission.delete(id);
|
await Permission.delete(id);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
})
|
})
|
@ -10,54 +10,60 @@ const RegCodeRoute: Router = Router();
|
|||||||
RegCodeRoute.route("/")
|
RegCodeRoute.route("/")
|
||||||
/**
|
/**
|
||||||
* @api {get} /admin/regcode
|
* @api {get} /admin/regcode
|
||||||
* @apiName AdminGetRegcodes
|
* @apiName AdminGetRegcodes
|
||||||
*
|
*
|
||||||
* @apiGroup admin_regcode
|
* @apiGroup admin_regcode
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object[]} regcodes
|
* @apiSuccess {Object[]} regcodes
|
||||||
* @apiSuccess {String} permissions._id The ID
|
* @apiSuccess {String} permissions._id The ID
|
||||||
* @apiSuccess {String} permissions.token The Regcode Token
|
* @apiSuccess {String} permissions.token The Regcode Token
|
||||||
* @apiSuccess {String} permissions.valid Defines if the Regcode is valid
|
* @apiSuccess {String} permissions.valid Defines if the Regcode is valid
|
||||||
* @apiSuccess {String} permissions.validTill Expiration date of RegCode
|
* @apiSuccess {String} permissions.validTill Expiration date of RegCode
|
||||||
*/
|
*/
|
||||||
.get(promiseMiddleware(async (req, res) => {
|
.get(
|
||||||
let regcodes = await RegCode.find({});
|
promiseMiddleware(async (req, res) => {
|
||||||
res.json(regcodes);
|
let regcodes = await RegCode.find({});
|
||||||
}))
|
res.json(regcodes);
|
||||||
|
})
|
||||||
|
)
|
||||||
/**
|
/**
|
||||||
* @api {delete} /admin/regcode
|
* @api {delete} /admin/regcode
|
||||||
* @apiName AdminDeleteRegcode
|
* @apiName AdminDeleteRegcode
|
||||||
*
|
*
|
||||||
* @apiParam {String} id The id of the RegCode
|
* @apiParam {String} id The id of the RegCode
|
||||||
*
|
*
|
||||||
* @apiGroup admin_regcode
|
* @apiGroup admin_regcode
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {Boolean} success
|
* @apiSuccess {Boolean} success
|
||||||
*/
|
*/
|
||||||
.delete(promiseMiddleware(async (req, res) => {
|
.delete(
|
||||||
let { id } = req.query;
|
promiseMiddleware(async (req, res) => {
|
||||||
await RegCode.delete(id);
|
let { id } = req.query as { [key: string]: string };
|
||||||
res.json({ success: true });
|
await RegCode.delete(id);
|
||||||
}))
|
res.json({ success: true });
|
||||||
|
})
|
||||||
|
)
|
||||||
/**
|
/**
|
||||||
* @api {post} /admin/regcode
|
* @api {post} /admin/regcode
|
||||||
* @apiName AdminAddRegcode
|
* @apiName AdminAddRegcode
|
||||||
*
|
*
|
||||||
* @apiGroup admin_regcode
|
* @apiGroup admin_regcode
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {String} code The newly created code
|
* @apiSuccess {String} code The newly created code
|
||||||
*/
|
*/
|
||||||
.post(promiseMiddleware(async (req, res) => {
|
.post(
|
||||||
let regcode = RegCode.new({
|
promiseMiddleware(async (req, res) => {
|
||||||
token: randomBytes(10).toString("hex"),
|
let regcode = RegCode.new({
|
||||||
valid: true,
|
token: randomBytes(10).toString("hex"),
|
||||||
validTill: moment().add("1", "month").toDate()
|
valid: true,
|
||||||
|
validTill: moment().add("1", "month").toDate(),
|
||||||
|
});
|
||||||
|
await RegCode.save(regcode);
|
||||||
|
res.json({ code: regcode.token });
|
||||||
})
|
})
|
||||||
await RegCode.save(regcode);
|
);
|
||||||
res.json({ code: regcode.token });
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default RegCodeRoute;
|
export default RegCodeRoute;
|
@ -9,15 +9,15 @@ import LoginToken from "../../models/login_token";
|
|||||||
|
|
||||||
const UserRoute: Router = Router();
|
const UserRoute: Router = Router();
|
||||||
UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
|
UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
|
||||||
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
|
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN);
|
||||||
else next()
|
else next();
|
||||||
})
|
});
|
||||||
|
|
||||||
UserRoute.route("/")
|
UserRoute.route("/")
|
||||||
/**
|
/**
|
||||||
* @api {get} /admin/user
|
* @api {get} /admin/user
|
||||||
* @apiName AdminGetUsers
|
* @apiName AdminGetUsers
|
||||||
*
|
*
|
||||||
* @apiGroup admin_user
|
* @apiGroup admin_user
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
* @apiSuccess {Object[]} user
|
* @apiSuccess {Object[]} user
|
||||||
@ -29,57 +29,65 @@ UserRoute.route("/")
|
|||||||
* @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other
|
* @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other
|
||||||
* @apiSuccess {Boolean} user.admin Is admin or not
|
* @apiSuccess {Boolean} user.admin Is admin or not
|
||||||
*/
|
*/
|
||||||
.get(promiseMiddleware(async (req, res) => {
|
.get(
|
||||||
let users = await User.find({});
|
promiseMiddleware(async (req, res) => {
|
||||||
users.forEach(e => delete e.password && delete e.salt && delete e.encryption_key);
|
let users = await User.find({});
|
||||||
res.json(users);
|
users.forEach(
|
||||||
}))
|
(e) => delete e.password && delete e.salt && delete e.encryption_key
|
||||||
|
);
|
||||||
|
res.json(users);
|
||||||
|
})
|
||||||
|
)
|
||||||
/**
|
/**
|
||||||
* @api {delete} /admin/user
|
* @api {delete} /admin/user
|
||||||
* @apiName AdminDeleteUser
|
* @apiName AdminDeleteUser
|
||||||
*
|
*
|
||||||
* @apiParam {String} id The User ID
|
* @apiParam {String} id The User ID
|
||||||
*
|
*
|
||||||
* @apiGroup admin_user
|
* @apiGroup admin_user
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {Boolean} success
|
* @apiSuccess {Boolean} success
|
||||||
*/
|
*/
|
||||||
.delete(promiseMiddleware(async (req, res) => {
|
.delete(
|
||||||
let { id } = req.query;
|
promiseMiddleware(async (req, res) => {
|
||||||
let user = await User.findById(id);
|
let { id } = req.query as { [key: string]: string };
|
||||||
|
let user = await User.findById(id);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
user.mails.map(mail => Mail.delete(mail)),
|
user.mails.map((mail) => Mail.delete(mail)),
|
||||||
[
|
[
|
||||||
RefreshToken.deleteFilter({ user: user._id }),
|
RefreshToken.deleteFilter({ user: user._id }),
|
||||||
LoginToken.deleteFilter({ user: user._id })
|
LoginToken.deleteFilter({ user: user._id }),
|
||||||
]
|
],
|
||||||
])
|
]);
|
||||||
|
|
||||||
await User.delete(user);
|
await User.delete(user);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
/**
|
/**
|
||||||
* @api {put} /admin/user
|
* @api {put} /admin/user
|
||||||
* @apiName AdminChangeUser
|
* @apiName AdminChangeUser
|
||||||
*
|
*
|
||||||
* @apiParam {String} id The User ID
|
* @apiParam {String} id The User ID
|
||||||
*
|
*
|
||||||
* @apiGroup admin_user
|
* @apiGroup admin_user
|
||||||
* @apiPermission admin
|
* @apiPermission admin
|
||||||
*
|
*
|
||||||
* @apiSuccess {Boolean} success
|
* @apiSuccess {Boolean} success
|
||||||
*
|
*
|
||||||
* @apiDescription Flipps the user role:
|
* @apiDescription Flipps the user role:
|
||||||
* admin -> user
|
* admin -> user
|
||||||
* user -> admin
|
* user -> admin
|
||||||
*/
|
*/
|
||||||
.put(promiseMiddleware(async (req, res) => {
|
.put(
|
||||||
let { id } = req.query;
|
promiseMiddleware(async (req, res) => {
|
||||||
let user = await User.findById(id);
|
let { id } = req.query as { [key: string]: string };
|
||||||
user.admin = !user.admin;
|
let user = await User.findById(id);
|
||||||
await User.save(user);
|
user.admin = !user.admin;
|
||||||
res.json({ success: true })
|
await User.save(user);
|
||||||
}))
|
res.json({ success: true });
|
||||||
export default UserRoute;
|
})
|
||||||
|
);
|
||||||
|
export default UserRoute;
|
110
Backend/src/api/client/index.ts
Normal file
110
Backend/src/api/client/index.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
import Stacker from "../middlewares/stacker";
|
||||||
|
import {
|
||||||
|
GetClientAuthMiddleware,
|
||||||
|
GetClientApiAuthMiddleware,
|
||||||
|
} from "../middlewares/client";
|
||||||
|
import { GetUserMiddleware } from "../middlewares/user";
|
||||||
|
import { createJWT } from "../../keys";
|
||||||
|
import Client from "../../models/client";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||||
|
import config from "../../config";
|
||||||
|
import Mail from "../../models/mail";
|
||||||
|
|
||||||
|
const ClientRouter = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /client/user
|
||||||
|
*
|
||||||
|
* @apiDescription Can be used for simple authentication of user. It will redirect the user to the redirect URI with a very short lived jwt.
|
||||||
|
*
|
||||||
|
* @apiParam {String} redirect_uri URL to redirect to on success
|
||||||
|
* @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter
|
||||||
|
*
|
||||||
|
* @apiName ClientUser
|
||||||
|
* @apiGroup client
|
||||||
|
*
|
||||||
|
* @apiPermission user_client Requires ClientID and Authenticated User
|
||||||
|
*/
|
||||||
|
ClientRouter.get(
|
||||||
|
"/user",
|
||||||
|
Stacker(
|
||||||
|
GetClientAuthMiddleware(false),
|
||||||
|
GetUserMiddleware(false, false),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let { redirect_uri, state } = req.query;
|
||||||
|
|
||||||
|
if (redirect_uri !== req.client.redirect_url)
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid redirect URI",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
|
||||||
|
let jwt = await createJWT(
|
||||||
|
{
|
||||||
|
client: req.client.client_id,
|
||||||
|
uid: req.user.uid,
|
||||||
|
username: req.user.username,
|
||||||
|
state: state,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expiresIn: 30,
|
||||||
|
issuer: config.core.url,
|
||||||
|
algorithm: "RS256",
|
||||||
|
subject: req.user.uid,
|
||||||
|
audience: req.client.client_id,
|
||||||
|
}
|
||||||
|
); //after 30 seconds this token is invalid
|
||||||
|
res.redirect(
|
||||||
|
redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
ClientRouter.get(
|
||||||
|
"/account",
|
||||||
|
Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => {
|
||||||
|
let mails = await Promise.all(
|
||||||
|
req.user.mails.map((id) => Mail.findById(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
let mail = mails.find((e) => e.primary) || mails[0];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
user: {
|
||||||
|
username: req.user.username,
|
||||||
|
name: req.user.name,
|
||||||
|
email: mail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /client/featured
|
||||||
|
*
|
||||||
|
* @apiDescription Get a list of clients, that want to be featured on the home page
|
||||||
|
*
|
||||||
|
* @apiName GetFeaturedClients
|
||||||
|
* @apiGroup client
|
||||||
|
*/
|
||||||
|
ClientRouter.get(
|
||||||
|
"/featured",
|
||||||
|
Stacker(async (req: Request, res) => {
|
||||||
|
let clients = await Client.find({
|
||||||
|
featured: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
clients: clients.map(({ name, logo, website, description }) => ({
|
||||||
|
name,
|
||||||
|
logo,
|
||||||
|
website,
|
||||||
|
description,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ClientRouter;
|
@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
|||||||
import Stacker from "../middlewares/stacker";
|
import Stacker from "../middlewares/stacker";
|
||||||
import {
|
import {
|
||||||
ClientAuthMiddleware,
|
ClientAuthMiddleware,
|
||||||
GetClientAuthMiddleware
|
GetClientAuthMiddleware,
|
||||||
} from "../middlewares/client";
|
} from "../middlewares/client";
|
||||||
import Permission from "../../models/permissions";
|
import Permission from "../../models/permissions";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
@ -14,7 +14,7 @@ import { ObjectID } from "mongodb";
|
|||||||
export const GetPermissions = Stacker(
|
export const GetPermissions = Stacker(
|
||||||
GetClientAuthMiddleware(true),
|
GetClientAuthMiddleware(true),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { user, permission } = req.query;
|
const { user, permission } = req.query as { [key: string]: string };
|
||||||
|
|
||||||
let permissions: { id: string; name: string; description: string }[];
|
let permissions: { id: string; name: string; description: string }[];
|
||||||
let users: string[];
|
let users: string[];
|
||||||
@ -22,19 +22,19 @@ export const GetPermissions = Stacker(
|
|||||||
if (user) {
|
if (user) {
|
||||||
const grant = await Grant.findOne({
|
const grant = await Grant.findOne({
|
||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
user: user
|
user: new ObjectID(user),
|
||||||
});
|
});
|
||||||
|
|
||||||
permissions = await Promise.all(
|
permissions = await Promise.all(
|
||||||
grant.permissions.map(perm => Permission.findById(perm))
|
grant.permissions.map((perm) => Permission.findById(perm))
|
||||||
).then(res =>
|
).then((res) =>
|
||||||
res
|
res
|
||||||
.filter(e => e.grant_type === "client")
|
.filter((e) => e.grant_type === "client")
|
||||||
.map(e => {
|
.map((e) => {
|
||||||
return {
|
return {
|
||||||
id: e._id.toHexString(),
|
id: e._id.toHexString(),
|
||||||
name: e.name,
|
name: e.name,
|
||||||
description: e.description
|
description: e.description,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -43,10 +43,10 @@ export const GetPermissions = Stacker(
|
|||||||
if (permission) {
|
if (permission) {
|
||||||
const grants = await Grant.find({
|
const grants = await Grant.find({
|
||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
permissions: new ObjectID(permission)
|
permissions: new ObjectID(permission),
|
||||||
});
|
});
|
||||||
|
|
||||||
users = grants.map(grant => grant.user.toHexString());
|
users = grants.map((grant) => grant.user.toHexString());
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ permissions, users });
|
res.json({ permissions, users });
|
||||||
@ -73,14 +73,14 @@ export const PostPermissions = Stacker(
|
|||||||
|
|
||||||
let grant = await Grant.findOne({
|
let grant = await Grant.findOne({
|
||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
user: req.user._id
|
user: req.user._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!grant) {
|
if (!grant) {
|
||||||
grant = Grant.new({
|
grant = Grant.new({
|
||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
permissions: []
|
permissions: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export const PostPermissions = Stacker(
|
|||||||
await Grant.save(grant);
|
await Grant.save(grant);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true
|
success: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -1,25 +1,33 @@
|
|||||||
import * as express from "express"
|
import * as express from "express";
|
||||||
import AdminRoute from "./admin";
|
import AdminRoute from "./admin";
|
||||||
import UserRoute from "./user";
|
import UserRoute from "./user";
|
||||||
import InternalRoute from "./internal";
|
import InternalRoute from "./internal";
|
||||||
import Login from "./user/login";
|
import Login from "./user/login";
|
||||||
import ClientRouter from "./client";
|
import ClientRouter from "./client";
|
||||||
import * as cors from "cors";
|
import * as cors from "cors";
|
||||||
import OAuthRoute from "./oauth";
|
import OAuthRoute from "./oauth";
|
||||||
|
import config from "../config";
|
||||||
const ApiRouter: express.IRouter = express.Router();
|
|
||||||
ApiRouter.use("/admin", AdminRoute);
|
const ApiRouter: express.IRouter = express.Router();
|
||||||
ApiRouter.use(cors())
|
ApiRouter.use("/admin", AdminRoute);
|
||||||
ApiRouter.use("/user", UserRoute);
|
ApiRouter.use(cors());
|
||||||
ApiRouter.use("/internal", InternalRoute);
|
ApiRouter.use("/user", UserRoute);
|
||||||
ApiRouter.use("/oauth", OAuthRoute);
|
ApiRouter.use("/internal", InternalRoute);
|
||||||
|
ApiRouter.use("/oauth", OAuthRoute);
|
||||||
ApiRouter.use("/client", ClientRouter);
|
|
||||||
|
ApiRouter.use("/client", ClientRouter);
|
||||||
// Legacy reasons (deprecated)
|
|
||||||
ApiRouter.use("/", ClientRouter);
|
// Legacy reasons (deprecated)
|
||||||
|
ApiRouter.use("/", ClientRouter);
|
||||||
// Legacy reasons (deprecated)
|
|
||||||
ApiRouter.post("/login", Login);
|
// Legacy reasons (deprecated)
|
||||||
|
ApiRouter.post("/login", Login);
|
||||||
export default ApiRouter;
|
|
||||||
|
ApiRouter.get("/config.json", (req, res) => {
|
||||||
|
return res.json({
|
||||||
|
name: config.core.name,
|
||||||
|
url: config.core.url,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ApiRouter;
|
@ -6,10 +6,10 @@ const InternalRoute: Router = Router();
|
|||||||
/**
|
/**
|
||||||
* @api {get} /internal/oauth
|
* @api {get} /internal/oauth
|
||||||
* @apiName ClientInteralOAuth
|
* @apiName ClientInteralOAuth
|
||||||
*
|
*
|
||||||
* @apiGroup client_internal
|
* @apiGroup client_internal
|
||||||
* @apiPermission client_internal Only ClientID
|
* @apiPermission client_internal Only ClientID
|
||||||
*
|
*
|
||||||
* @apiParam {String} redirect_uri Redirect URI called after success
|
* @apiParam {String} redirect_uri Redirect URI called after success
|
||||||
* @apiParam {String} state State will be set in RedirectURI for the client to check
|
* @apiParam {String} state State will be set in RedirectURI for the client to check
|
||||||
*/
|
*/
|
||||||
@ -18,13 +18,13 @@ InternalRoute.get("/oauth", OAuthInternalApp);
|
|||||||
/**
|
/**
|
||||||
* @api {post} /internal/password
|
* @api {post} /internal/password
|
||||||
* @apiName ClientInteralPassword
|
* @apiName ClientInteralPassword
|
||||||
*
|
*
|
||||||
* @apiGroup client_internal
|
* @apiGroup client_internal
|
||||||
* @apiPermission client_internal Requires ClientID and Secret
|
* @apiPermission client_internal Requires ClientID and Secret
|
||||||
*
|
*
|
||||||
* @apiParam {String} username Username (either username or UID)
|
* @apiParam {String} username Username (either username or UID)
|
||||||
* @apiParam {String} uid User ID (either username or UID)
|
* @apiParam {String} uid User ID (either username or UID)
|
||||||
* @apiParam {String} password Hashed and Salted according to specification
|
* @apiParam {String} password Hashed and Salted according to specification
|
||||||
*/
|
*/
|
||||||
InternalRoute.post("/password", PasswordAuth)
|
InternalRoute.post("/password", PasswordAuth);
|
||||||
export default InternalRoute;
|
export default InternalRoute;
|
@ -6,11 +6,16 @@ import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
|||||||
import ClientCode from "../../models/client_code";
|
import ClientCode from "../../models/client_code";
|
||||||
import moment = require("moment");
|
import moment = require("moment");
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
export const OAuthInternalApp = Stacker(GetClientAuthMiddleware(false, true), UserMiddleware,
|
export const OAuthInternalApp = Stacker(
|
||||||
|
GetClientAuthMiddleware(false, true),
|
||||||
|
UserMiddleware,
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
let { redirect_uri, state } = req.query
|
let { redirect_uri, state } = req.query as { [key: string]: string };
|
||||||
if (!redirect_uri) {
|
if (!redirect_uri) {
|
||||||
throw new RequestError("No redirect url set!", HttpStatusCode.BAD_REQUEST);
|
throw new RequestError(
|
||||||
|
"No redirect url set!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&";
|
let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&";
|
||||||
@ -20,10 +25,17 @@ export const OAuthInternalApp = Stacker(GetClientAuthMiddleware(false, true), Us
|
|||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
validTill: moment().add(30, "minutes").toDate(),
|
validTill: moment().add(30, "minutes").toDate(),
|
||||||
code: randomBytes(16).toString("hex"),
|
code: randomBytes(16).toString("hex"),
|
||||||
permissions: []
|
permissions: [],
|
||||||
});
|
});
|
||||||
await ClientCode.save(code);
|
await ClientCode.save(code);
|
||||||
|
|
||||||
res.redirect(redirect_uri + sep + "code=" + code.code + (state ? "&state=" + state : ""));
|
res.redirect(
|
||||||
|
redirect_uri +
|
||||||
|
sep +
|
||||||
|
"code=" +
|
||||||
|
code.code +
|
||||||
|
(state ? "&state=" + state : "")
|
||||||
|
);
|
||||||
res.end();
|
res.end();
|
||||||
});
|
}
|
||||||
|
);
|
35
Backend/src/api/internal/password.ts
Normal file
35
Backend/src/api/internal/password.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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;
|
@ -5,8 +5,13 @@ import { validateJWT } from "../../keys";
|
|||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import Mail from "../../models/mail";
|
import Mail from "../../models/mail";
|
||||||
import { OAuthJWT } from "../../helper/jwt";
|
import { OAuthJWT } from "../../helper/jwt";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
|
||||||
export function GetClientAuthMiddleware(checksecret = true, internal = false, checksecret_if_available = false) {
|
export function GetClientAuthMiddleware(
|
||||||
|
checksecret = true,
|
||||||
|
internal = false,
|
||||||
|
checksecret_if_available = false
|
||||||
|
) {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
let client_id = req.query.client_id || req.body.client_id;
|
let client_id = req.query.client_id || req.body.client_id;
|
||||||
@ -24,19 +29,29 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!client_id || (!client_secret && checksecret)) {
|
if (!client_id || (!client_secret && checksecret)) {
|
||||||
throw new RequestError("No client credentials", HttpStatusCode.BAD_REQUEST);
|
throw new RequestError(
|
||||||
|
"No client credentials",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let w = { client_id: client_id, client_secret: client_secret };
|
let w = { client_id: client_id, client_secret: client_secret };
|
||||||
if (!checksecret && !(checksecret_if_available && client_secret)) delete w.client_secret;
|
if (!checksecret && !(checksecret_if_available && client_secret))
|
||||||
|
delete w.client_secret;
|
||||||
|
|
||||||
let client = await Client.findOne(w)
|
let client = await Client.findOne(w);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new RequestError("Invalid client_id" + (checksecret ? "or client_secret" : ""), HttpStatusCode.BAD_REQUEST);
|
throw new RequestError(
|
||||||
|
"Invalid client_id" + (checksecret ? "or client_secret" : ""),
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internal && !client.internal) {
|
if (internal && !client.internal) {
|
||||||
throw new RequestError(req.__("Client has no permission for access"), HttpStatusCode.FORBIDDEN)
|
throw new RequestError(
|
||||||
|
req.__("Client has no permission for access"),
|
||||||
|
HttpStatusCode.FORBIDDEN
|
||||||
|
);
|
||||||
}
|
}
|
||||||
req.client = client;
|
req.client = client;
|
||||||
next();
|
next();
|
||||||
@ -44,7 +59,7 @@ export function GetClientAuthMiddleware(checksecret = true, internal = false, ch
|
|||||||
if (next) next(e);
|
if (next) next(e);
|
||||||
else throw e;
|
else throw e;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClientAuthMiddleware = GetClientAuthMiddleware();
|
export const ClientAuthMiddleware = GetClientAuthMiddleware();
|
||||||
@ -52,10 +67,17 @@ export const ClientAuthMiddleware = GetClientAuthMiddleware();
|
|||||||
export function GetClientApiAuthMiddleware(permissions?: string[]) {
|
export function GetClientApiAuthMiddleware(permissions?: string[]) {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const invalid_err = new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED);
|
const invalid_err = new RequestError(
|
||||||
let token: string = req.query.access_token || req.headers.authorization;
|
req.__("Unauthorized"),
|
||||||
if (!token)
|
HttpStatusCode.UNAUTHORIZED
|
||||||
|
);
|
||||||
|
let token =
|
||||||
|
(req.query.access_token as string) ||
|
||||||
|
(req.headers.authorization as string);
|
||||||
|
if (!token) {
|
||||||
|
Logging.debug("No token found. Searched in query (access_token) and header (authorization)");
|
||||||
throw invalid_err;
|
throw invalid_err;
|
||||||
|
}
|
||||||
|
|
||||||
if (token.toLowerCase().startsWith("bearer "))
|
if (token.toLowerCase().startsWith("bearer "))
|
||||||
token = token.substring(7);
|
token = token.substring(7);
|
||||||
@ -64,20 +86,31 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) {
|
|||||||
try {
|
try {
|
||||||
data = await validateJWT(token);
|
data = await validateJWT(token);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw invalid_err
|
Logging.debug("Invalid JWT", err.message);
|
||||||
|
throw invalid_err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await User.findOne({ uid: data.user });
|
let user = await User.findOne({ uid: data.user });
|
||||||
|
|
||||||
if (!user)
|
if (!user) {
|
||||||
|
Logging.debug("User not found");
|
||||||
throw invalid_err;
|
throw invalid_err;
|
||||||
|
}
|
||||||
|
|
||||||
let client = await Client.findOne({ client_id: data.application })
|
let client = await Client.findOne({ client_id: data.application });
|
||||||
if (!client)
|
if (!client) {
|
||||||
|
Logging.debug("Client not found");
|
||||||
throw invalid_err;
|
throw invalid_err;
|
||||||
|
}
|
||||||
|
|
||||||
if (permissions && (!data.permissions || !permissions.every(e => data.permissions.indexOf(e) >= 0)))
|
if (
|
||||||
|
permissions &&
|
||||||
|
(!data.permissions ||
|
||||||
|
!permissions.every((e) => data.permissions.indexOf(e) >= 0))
|
||||||
|
) {
|
||||||
|
Logging.debug("Invalid permissions");
|
||||||
throw invalid_err;
|
throw invalid_err;
|
||||||
|
}
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
req.client = client;
|
req.client = client;
|
||||||
@ -86,5 +119,5 @@ export function GetClientApiAuthMiddleware(permissions?: string[]) {
|
|||||||
if (next) next(e);
|
if (next) next(e);
|
||||||
else throw e;
|
else throw e;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
@ -4,23 +4,25 @@ import promiseMiddleware from "../../helper/promiseMiddleware";
|
|||||||
type RH = (req: Request, res: Response, next?: NextFunction) => any;
|
type RH = (req: Request, res: Response, next?: NextFunction) => any;
|
||||||
|
|
||||||
function call(handler: RH, req: Request, res: Response) {
|
function call(handler: RH, req: Request, res: Response) {
|
||||||
return new Promise((yes, no) => {
|
return new Promise<void>((yes, no) => {
|
||||||
let p = handler(req, res, (err) => {
|
let p = handler(req, res, (err) => {
|
||||||
if (err) no(err);
|
if (err) no(err);
|
||||||
else yes();
|
else yes();
|
||||||
})
|
});
|
||||||
if (p && p.catch) p.catch(err => no(err));
|
if (p && p.catch) p.catch((err) => no(err));
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const Stacker = (...handler: RH[]) => {
|
const Stacker = (...handler: RH[]) => {
|
||||||
return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => {
|
return promiseMiddleware(
|
||||||
let hc = handler.concat();
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
while (hc.length > 0) {
|
let hc = handler.concat();
|
||||||
let h = hc.shift();
|
while (hc.length > 0) {
|
||||||
await call(h, req, res);
|
let h = hc.shift();
|
||||||
|
await call(h, req, res);
|
||||||
|
}
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
next();
|
);
|
||||||
});
|
};
|
||||||
}
|
export default Stacker;
|
||||||
export default Stacker;
|
|
@ -1,110 +1,106 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import LoginToken, { CheckToken, ILoginToken } from "../../models/login_token";
|
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||||
import Logging from "@hibas123/nodelogging";
|
import Logging from "@hibas123/nodelogging";
|
||||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||||
|
|
||||||
class Invalid extends Error {}
|
class Invalid extends Error {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns customized Middleware function, that could also be called directly
|
* Returns customized Middleware function, that could also be called directly
|
||||||
* by code and will return true or false depending on the token. In the false
|
* by code and will return true or false depending on the token. In the false
|
||||||
* case it will also send error and redirect if json is not set
|
* case it will also send error and redirect if json is not set
|
||||||
* @param json Default false. Checks if requests wants an json or html for returning errors
|
* @param json Default false. Checks if requests wants an json or html for returning errors
|
||||||
* @param special_required Default false. If true, a special token is required
|
* @param special_required Default false. If true, a special token is required
|
||||||
* @param redirect_uri Default current uri. Sets the uri to redirect, if json is not set and user not logged in
|
* @param redirect_uri Default current uri. Sets the uri to redirect, if json is not set and user not logged in
|
||||||
* @param validated Default true. If false, the token must not be validated
|
* @param validated Default true. If false, the token must not be validated
|
||||||
*/
|
*/
|
||||||
export function GetUserMiddleware(
|
export function GetUserMiddleware(
|
||||||
json = false,
|
json = false,
|
||||||
special_required: boolean = false,
|
special_required: boolean = false,
|
||||||
redirect_uri?: string,
|
redirect_uri?: string,
|
||||||
validated = true
|
validated = true
|
||||||
) {
|
) {
|
||||||
return promiseMiddleware(async function (
|
return promiseMiddleware(async function (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next?: NextFunction
|
next?: NextFunction
|
||||||
) {
|
) {
|
||||||
const invalid = (message: string) => {
|
const invalid = (message: string) => {
|
||||||
throw new Invalid(req.__(message));
|
throw new Invalid(req.__(message));
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
let { login, special } = req.query;
|
let { login, special } = req.query as { [key: string]: string };
|
||||||
if (!login) {
|
if (!login) {
|
||||||
login = req.cookies.login;
|
login = req.cookies.login;
|
||||||
special = req.cookies.special;
|
special = req.cookies.special;
|
||||||
}
|
}
|
||||||
if (!login) invalid("No login token");
|
if (!login) invalid("No login token");
|
||||||
if (!special && special_required) invalid("No special token");
|
if (!special && special_required) invalid("No special token");
|
||||||
|
|
||||||
let token = await LoginToken.findOne({ token: login, valid: true });
|
let token = await LoginToken.findOne({ token: login, valid: true });
|
||||||
if (!(await CheckToken(token, validated)))
|
if (!(await CheckToken(token, validated)))
|
||||||
invalid("Login token invalid");
|
invalid("Login token invalid");
|
||||||
|
|
||||||
let user = await User.findById(token.user);
|
let user = await User.findById(token.user);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
token.valid = false;
|
token.valid = false;
|
||||||
await LoginToken.save(token);
|
await LoginToken.save(token);
|
||||||
invalid("Login token invalid");
|
invalid("Login token invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
let special_token: ILoginToken;
|
let special_token;
|
||||||
let session: any;
|
if (special) {
|
||||||
if (special) {
|
Logging.debug("Special found");
|
||||||
Logging.debug("Special found");
|
special_token = await LoginToken.findOne({
|
||||||
special_token = await LoginToken.findOne({
|
token: special,
|
||||||
token: special,
|
special: true,
|
||||||
special: true,
|
valid: true,
|
||||||
valid: true,
|
user: token.user,
|
||||||
user: token.user,
|
});
|
||||||
});
|
if (!(await CheckToken(special_token, validated)))
|
||||||
if (!(await CheckToken(special_token, validated)))
|
invalid("Special token invalid");
|
||||||
invalid("Special token invalid");
|
req.special = true;
|
||||||
req.special = true;
|
}
|
||||||
|
|
||||||
//
|
req.user = user;
|
||||||
}
|
req.isAdmin = user.admin;
|
||||||
|
req.token = {
|
||||||
req.user = user;
|
login: token,
|
||||||
req.isAdmin = user.admin;
|
special: special_token,
|
||||||
req.token = {
|
};
|
||||||
login: token,
|
|
||||||
special: special_token,
|
if (next) next();
|
||||||
session,
|
return true;
|
||||||
};
|
} catch (e) {
|
||||||
|
if (e instanceof Invalid) {
|
||||||
if (next) next();
|
if (req.method === "GET" && !json) {
|
||||||
return true;
|
res.status(HttpStatusCode.UNAUTHORIZED);
|
||||||
} catch (e) {
|
res.redirect(
|
||||||
if (e instanceof Invalid) {
|
"/login?base64=true&state=" +
|
||||||
if (req.method === "GET" && !json) {
|
Buffer.from(
|
||||||
res.status(HttpStatusCode.UNAUTHORIZED);
|
redirect_uri ? redirect_uri : req.originalUrl
|
||||||
res.redirect(
|
).toString("base64")
|
||||||
"/login?base64=true&state=" +
|
);
|
||||||
Buffer.from(
|
} else {
|
||||||
redirect_uri ? redirect_uri : req.originalUrl
|
throw new RequestError(
|
||||||
).toString("base64")
|
req.__(
|
||||||
);
|
"You are not logged in or your login is expired" +
|
||||||
} else {
|
` (${e.message})`
|
||||||
throw new RequestError(
|
),
|
||||||
req.__(
|
HttpStatusCode.UNAUTHORIZED,
|
||||||
"You are not logged in or your login is expired" +
|
undefined,
|
||||||
` (${e.message})`
|
{ auth: true }
|
||||||
),
|
);
|
||||||
HttpStatusCode.UNAUTHORIZED,
|
}
|
||||||
undefined,
|
} else {
|
||||||
{ auth: true }
|
if (next) next(e);
|
||||||
);
|
else throw e;
|
||||||
}
|
}
|
||||||
} else {
|
return false;
|
||||||
if (next) next(e);
|
}
|
||||||
else throw e;
|
});
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
export const UserMiddleware = GetUserMiddleware();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserMiddleware = GetUserMiddleware();
|
|
@ -1,6 +1,9 @@
|
|||||||
import { Request, Response, NextFunction } from "express"
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { Logging } from "@hibas123/nodelogging";
|
import Logging from "@hibas123/nodelogging";
|
||||||
import { isBoolean, isString, isNumber, isObject, isDate, isArray, isSymbol } from "util";
|
import {
|
||||||
|
isString,
|
||||||
|
isDate,
|
||||||
|
} from "util";
|
||||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||||
|
|
||||||
export enum Types {
|
export enum Types {
|
||||||
@ -11,39 +14,41 @@ export enum Types {
|
|||||||
OBJECT,
|
OBJECT,
|
||||||
DATE,
|
DATE,
|
||||||
ARRAY,
|
ARRAY,
|
||||||
ENUM
|
ENUM,
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmail(value: any): boolean {
|
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)
|
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 {
|
export interface CheckObject {
|
||||||
type: Types
|
type: Types;
|
||||||
query?: boolean
|
query?: boolean;
|
||||||
optional?: boolean
|
optional?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only when Type.ENUM
|
* Only when Type.ENUM
|
||||||
*
|
*
|
||||||
* values to check before
|
* values to check before
|
||||||
*/
|
*/
|
||||||
values?: string[]
|
values?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only when Type.STRING
|
* Only when Type.STRING
|
||||||
*/
|
*/
|
||||||
notempty?: boolean // Only STRING
|
notempty?: boolean; // Only STRING
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Checks {
|
export interface Checks {
|
||||||
[index: string]: CheckObject// | Types
|
[index: string]: CheckObject; // | Types
|
||||||
}
|
}
|
||||||
|
|
||||||
// req: Request, res: Response, next: NextFunction
|
// req: Request, res: Response, next: NextFunction
|
||||||
export default function (fields: Checks, noadditional = false) {
|
export default function (fields: Checks, noadditional = false) {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
let errors: { message: string, field: string }[] = []
|
let errors: { message: string; field: string }[] = [];
|
||||||
|
|
||||||
function check(data: any, field_name: string, field: CheckObject) {
|
function check(data: any, field_name: string, field: CheckObject) {
|
||||||
if (data !== undefined && data !== null) {
|
if (data !== undefined && data !== null) {
|
||||||
@ -55,71 +60,83 @@ export default function (fields: Checks, noadditional = false) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Types.NUMBER:
|
case Types.NUMBER:
|
||||||
if (isNumber(data)) return;
|
if (typeof data == "number") return;
|
||||||
break;
|
break;
|
||||||
case Types.EMAIL:
|
case Types.EMAIL:
|
||||||
if (isEmail(data)) return;
|
if (isEmail(data)) return;
|
||||||
break;
|
break;
|
||||||
case Types.BOOLEAN:
|
case Types.BOOLEAN:
|
||||||
if (isBoolean(data)) return;
|
if (typeof data == "boolean") return;
|
||||||
break;
|
break;
|
||||||
case Types.OBJECT:
|
case Types.OBJECT:
|
||||||
if (isObject(data)) return;
|
if (typeof data == "object") return;
|
||||||
break;
|
break;
|
||||||
case Types.ARRAY:
|
case Types.ARRAY:
|
||||||
if (isArray(data)) return;
|
if (Array.isArray(data)) return;
|
||||||
break;
|
break;
|
||||||
case Types.DATE:
|
case Types.DATE:
|
||||||
if (isDate(data)) return;
|
if (isDate(data)) return;
|
||||||
break;
|
break;
|
||||||
case Types.ENUM:
|
case Types.ENUM:
|
||||||
if (isString(data)) {
|
if (typeof data == "string") {
|
||||||
if (field.values.indexOf(data) >= 0) return;
|
if (field.values.indexOf(data) >= 0) return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Logging.error(`Invalid type to check: ${field.type} ${Types[field.type]}`)
|
Logging.error(
|
||||||
|
`Invalid type to check: ${field.type} ${Types[field.type]}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
errors.push({
|
errors.push({
|
||||||
message: res.__("Field {{field}} has wrong type. It should be from type {{type}}", { field: field_name, type: Types[field.type].toLowerCase() }),
|
message: res.__(
|
||||||
field: field_name
|
"Field {{field}} has wrong type. It should be from type {{type}}",
|
||||||
})
|
{ field: field_name, type: Types[field.type].toLowerCase() }
|
||||||
|
),
|
||||||
|
field: field_name,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!field.optional) errors.push({
|
if (!field.optional)
|
||||||
message: res.__("Field {{field}} is not defined", { field: field_name }),
|
errors.push({
|
||||||
field: field_name
|
message: res.__("Field {{field}} is not defined", {
|
||||||
})
|
field: field_name,
|
||||||
|
}),
|
||||||
|
field: field_name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let field_name in fields) {
|
for (let field_name in fields) {
|
||||||
let field = fields[field_name]
|
let field = fields[field_name];
|
||||||
let data = fields[field_name].query ? req.query[field_name] : req.body[field_name]
|
let data = fields[field_name].query
|
||||||
check(data, field_name, field)
|
? req.query[field_name]
|
||||||
|
: req.body[field_name];
|
||||||
|
check(data, field_name, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noadditional) { //Checks if the data given has additional parameters
|
if (noadditional) {
|
||||||
|
//Checks if the data given has additional parameters
|
||||||
let should = Object.keys(fields);
|
let should = Object.keys(fields);
|
||||||
should = should.filter(e => !fields[e].query); //Query parameters should not exist on body
|
should = should.filter((e) => !fields[e].query); //Query parameters should not exist on body
|
||||||
let has = Object.keys(req.body);
|
let has = Object.keys(req.body);
|
||||||
|
|
||||||
has.every(e => {
|
has.every((e) => {
|
||||||
if (should.indexOf(e) >= 0) {
|
if (should.indexOf(e) >= 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
errors.push({
|
errors.push({
|
||||||
message: res.__("Field {{field}} should not be there", { field: e }),
|
message: res.__("Field {{field}} should not be there", {
|
||||||
field: e
|
field: e,
|
||||||
})
|
}),
|
||||||
|
field: e,
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true);
|
let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true);
|
||||||
next(err);
|
next(err);
|
||||||
} else
|
} else next();
|
||||||
next()
|
};
|
||||||
}
|
}
|
||||||
}
|
|
@ -91,16 +91,18 @@ const GetAuthRoute = (view = false) =>
|
|||||||
response_type,
|
response_type,
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
scope,
|
scope = "",
|
||||||
state,
|
state,
|
||||||
nored
|
nored,
|
||||||
} = req.query;
|
} = req.query as { [key: string]: string };
|
||||||
const sendError = type => {
|
const sendError = (type) => {
|
||||||
if (redirect_uri === "$local") redirect_uri = "/code";
|
if (redirect_uri === "$local") redirect_uri = "/code";
|
||||||
res.redirect((redirect_uri += `?error=${type}&state=${state}`));
|
res.redirect(
|
||||||
|
(redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scopes = scope.split(";");
|
const scopes = scope.split(";").filter((e: string) => e !== "");
|
||||||
|
|
||||||
Logging.debug("Scopes:", scope);
|
Logging.debug("Scopes:", scope);
|
||||||
|
|
||||||
@ -123,7 +125,7 @@ const GetAuthRoute = (view = false) =>
|
|||||||
let permissions: IPermission[] = [];
|
let permissions: IPermission[] = [];
|
||||||
let proms: PromiseLike<void>[] = [];
|
let proms: PromiseLike<void>[] = [];
|
||||||
if (scopes) {
|
if (scopes) {
|
||||||
for (let perm of scopes.filter(e => e !== "read_user")) {
|
for (let perm of scopes.filter((e) => e !== "read_user")) {
|
||||||
let oid = undefined;
|
let oid = undefined;
|
||||||
try {
|
try {
|
||||||
oid = new ObjectID(perm);
|
oid = new ObjectID(perm);
|
||||||
@ -132,7 +134,7 @@ const GetAuthRoute = (view = false) =>
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
proms.push(
|
proms.push(
|
||||||
Permission.findById(oid).then(p => {
|
Permission.findById(oid).then((p) => {
|
||||||
if (!p) return Promise.reject(new Error());
|
if (!p) return Promise.reject(new Error());
|
||||||
permissions.push(p);
|
permissions.push(p);
|
||||||
})
|
})
|
||||||
@ -141,7 +143,7 @@ const GetAuthRoute = (view = false) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
let err = undefined;
|
let err = undefined;
|
||||||
await Promise.all(proms).catch(e => {
|
await Promise.all(proms).catch((e) => {
|
||||||
err = e;
|
err = e;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -152,7 +154,7 @@ const GetAuthRoute = (view = false) =>
|
|||||||
|
|
||||||
let grant: IGrant | undefined = await Grant.findOne({
|
let grant: IGrant | undefined = await Grant.findOne({
|
||||||
client: client._id,
|
client: client._id,
|
||||||
user: req.user._id
|
user: req.user._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
Logging.debug("Grant", grant, permissions);
|
Logging.debug("Grant", grant, permissions);
|
||||||
@ -161,14 +163,14 @@ const GetAuthRoute = (view = false) =>
|
|||||||
|
|
||||||
if (grant) {
|
if (grant) {
|
||||||
missing_permissions = grant.permissions
|
missing_permissions = grant.permissions
|
||||||
.map(perm => permissions.find(p => p._id.equals(perm)))
|
.map((perm) => permissions.find((p) => p._id.equals(perm)))
|
||||||
.filter(e => !!e);
|
.filter((e) => !!e);
|
||||||
} else {
|
} else {
|
||||||
missing_permissions = permissions;
|
missing_permissions = permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
let client_granted_perm = missing_permissions.filter(
|
let client_granted_perm = missing_permissions.filter(
|
||||||
e => e.grant_type == "client"
|
(e) => e.grant_type == "client"
|
||||||
);
|
);
|
||||||
if (client_granted_perm.length > 0) {
|
if (client_granted_perm.length > 0) {
|
||||||
return sendError("no_permission");
|
return sendError("no_permission");
|
||||||
@ -176,8 +178,10 @@ const GetAuthRoute = (view = false) =>
|
|||||||
|
|
||||||
if (!grant && missing_permissions.length > 0) {
|
if (!grant && missing_permissions.length > 0) {
|
||||||
await new Promise<void>((yes, no) =>
|
await new Promise<void>((yes, no) =>
|
||||||
GetUserMiddleware(false, true)(req, res, (err?: Error) =>
|
GetUserMiddleware(false, true)(
|
||||||
err ? no(err) : yes()
|
req,
|
||||||
|
res,
|
||||||
|
(err?: Error | string) => (err ? no(err) : yes())
|
||||||
)
|
)
|
||||||
); // Maybe unresolved when redirect is happening
|
); // Maybe unresolved when redirect is happening
|
||||||
|
|
||||||
@ -186,11 +190,11 @@ const GetAuthRoute = (view = false) =>
|
|||||||
GetAuthPage(
|
GetAuthPage(
|
||||||
req.__,
|
req.__,
|
||||||
client.name,
|
client.name,
|
||||||
permissions.map(perm => {
|
permissions.map((perm) => {
|
||||||
return {
|
return {
|
||||||
name: perm.name,
|
name: perm.name,
|
||||||
description: perm.description,
|
description: perm.description,
|
||||||
logo: client.logo
|
logo: client.logo,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -202,11 +206,11 @@ const GetAuthRoute = (view = false) =>
|
|||||||
grant = Grant.new({
|
grant = Grant.new({
|
||||||
client: client._id,
|
client: client._id,
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
permissions: []
|
permissions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
grant.permissions.push(
|
grant.permissions.push(
|
||||||
...missing_permissions.map(e => e._id)
|
...missing_permissions.map((e) => e._id)
|
||||||
);
|
);
|
||||||
await Grant.save(grant);
|
await Grant.save(grant);
|
||||||
} else {
|
} else {
|
||||||
@ -218,20 +222,19 @@ const GetAuthRoute = (view = false) =>
|
|||||||
let code = ClientCode.new({
|
let code = ClientCode.new({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
client: client._id,
|
client: client._id,
|
||||||
permissions: permissions.map(p => p._id),
|
permissions: permissions.map((p) => p._id),
|
||||||
validTill: moment()
|
validTill: moment().add(30, "minutes").toDate(),
|
||||||
.add(30, "minutes")
|
code: randomBytes(16).toString("hex"),
|
||||||
.toDate(),
|
|
||||||
code: randomBytes(16).toString("hex")
|
|
||||||
});
|
});
|
||||||
await ClientCode.save(code);
|
await ClientCode.save(code);
|
||||||
|
|
||||||
let redir =
|
let redir =
|
||||||
client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||||
let ruri = redir + `?code=${code.code}&state=${state}`;
|
let ruri =
|
||||||
|
redir + `?code=${code.code}${state ? "&state=" + state : ""}`;
|
||||||
if (nored === "true") {
|
if (nored === "true") {
|
||||||
res.json({
|
res.json({
|
||||||
redirect_uri: ruri
|
redirect_uri: ruri,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.redirect(ruri);
|
res.redirect(ruri);
|
@ -3,15 +3,16 @@ import GetAuthRoute from "./auth";
|
|||||||
import JWTRoute from "./jwt";
|
import JWTRoute from "./jwt";
|
||||||
import Public from "./public";
|
import Public from "./public";
|
||||||
import RefreshTokenRoute from "./refresh";
|
import RefreshTokenRoute from "./refresh";
|
||||||
|
import ProfileRoute from "./profile";
|
||||||
|
|
||||||
const OAuthRoue: Router = Router();
|
const OAuthRoute: Router = Router();
|
||||||
/**
|
/**
|
||||||
* @api {post} /oauth/auth
|
* @api {post} /oauth/auth
|
||||||
* @apiName OAuthAuth
|
* @apiName OAuthAuth
|
||||||
*
|
*
|
||||||
* @apiGroup oauth
|
* @apiGroup oauth
|
||||||
* @apiPermission user Special required
|
* @apiPermission user Special required
|
||||||
*
|
*
|
||||||
* @apiParam {String} response_type must be "code" others are not supported
|
* @apiParam {String} response_type must be "code" others are not supported
|
||||||
* @apiParam {String} client_id ClientID
|
* @apiParam {String} client_id ClientID
|
||||||
* @apiParam {String} redirect_uri The URI to redirect with code
|
* @apiParam {String} redirect_uri The URI to redirect with code
|
||||||
@ -19,45 +20,54 @@ const OAuthRoue: Router = Router();
|
|||||||
* @apiParam {String} state State, that will be passed to redirect_uri for client
|
* @apiParam {String} state State, that will be passed to redirect_uri for client
|
||||||
* @apiParam {String} nored Deactivates the Redirect response from server and instead returns the redirect URI in JSON response
|
* @apiParam {String} nored Deactivates the Redirect response from server and instead returns the redirect URI in JSON response
|
||||||
*/
|
*/
|
||||||
OAuthRoue.post("/auth", GetAuthRoute(false));
|
OAuthRoute.post("/auth", GetAuthRoute(false));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /oauth/jwt
|
* @api {get} /oauth/jwt
|
||||||
* @apiName OAuthJwt
|
* @apiName OAuthJwt
|
||||||
*
|
*
|
||||||
* @apiGroup oauth
|
* @apiGroup oauth
|
||||||
* @apiPermission none
|
* @apiPermission none
|
||||||
*
|
*
|
||||||
* @apiParam {String} refreshtoken
|
* @apiParam {String} refreshtoken
|
||||||
*
|
*
|
||||||
* @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token
|
* @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token
|
||||||
*/
|
*/
|
||||||
OAuthRoue.get("/jwt", JWTRoute)
|
OAuthRoute.get("/jwt", JWTRoute);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /oauth/public
|
* @api {get} /oauth/public
|
||||||
* @apiName OAuthPublic
|
* @apiName OAuthPublic
|
||||||
*
|
*
|
||||||
* @apiGroup oauth
|
* @apiGroup oauth
|
||||||
* @apiPermission none
|
* @apiPermission none
|
||||||
*
|
*
|
||||||
* @apiSuccess {String} public_key The applications public_key. Used to verify JWT.
|
* @apiSuccess {String} public_key The applications public_key. Used to verify JWT.
|
||||||
*/
|
*/
|
||||||
OAuthRoue.get("/public", Public)
|
OAuthRoute.get("/public", Public);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /oauth/refresh
|
* @api {get} /oauth/refresh
|
||||||
* @apiName OAuthRefreshGet
|
* @apiName OAuthRefreshGet
|
||||||
*
|
*
|
||||||
* @apiGroup oauth
|
* @apiGroup oauth
|
||||||
*/
|
*/
|
||||||
OAuthRoue.get("/refresh", RefreshTokenRoute);
|
OAuthRoute.get("/refresh", RefreshTokenRoute);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /oauth/refresh
|
* @api {post} /oauth/refresh
|
||||||
* @apiName OAuthRefreshPost
|
* @apiName OAuthRefreshPost
|
||||||
|
*
|
||||||
|
* @apiGroup oauth
|
||||||
|
*/
|
||||||
|
OAuthRoute.post("/refresh", RefreshTokenRoute);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /oauth/profile
|
||||||
|
* @apiName OAuthProfile
|
||||||
*
|
*
|
||||||
* @apiGroup oauth
|
* @apiGroup oauth
|
||||||
*/
|
*/
|
||||||
OAuthRoue.post("/refresh", RefreshTokenRoute);
|
OAuthRoute.get("/profile", ProfileRoute);
|
||||||
export default OAuthRoue;
|
|
||||||
|
export default OAuthRoute;
|
@ -7,22 +7,37 @@ import Client from "../../models/client";
|
|||||||
import { getAccessTokenJWT } from "../../helper/jwt";
|
import { getAccessTokenJWT } from "../../helper/jwt";
|
||||||
|
|
||||||
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
|
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
|
||||||
let { refreshtoken } = req.query;
|
let { refreshtoken } = req.query as { [key: string]: string };
|
||||||
if (!refreshtoken) throw new RequestError(req.__("Refresh token not set"), HttpStatusCode.BAD_REQUEST);
|
if (!refreshtoken)
|
||||||
|
throw new RequestError(
|
||||||
|
req.__("Refresh token not set"),
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
|
||||||
let token = await RefreshToken.findOne({ token: refreshtoken });
|
let token = await RefreshToken.findOne({ token: refreshtoken });
|
||||||
if (!token) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
|
if (!token)
|
||||||
|
throw new RequestError(
|
||||||
|
req.__("Invalid token"),
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
|
||||||
let user = await User.findById(token.user);
|
let user = await User.findById(token.user);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
token.valid = false;
|
token.valid = false;
|
||||||
await RefreshToken.save(token);
|
await RefreshToken.save(token);
|
||||||
throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
|
throw new RequestError(
|
||||||
|
req.__("Invalid token"),
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = await Client.findById(token.client);
|
let client = await Client.findById(token.client);
|
||||||
|
|
||||||
let jwt = await getAccessTokenJWT({ user, permissions: token.permissions, client });
|
let jwt = await getAccessTokenJWT({
|
||||||
|
user,
|
||||||
|
permissions: token.permissions,
|
||||||
|
client,
|
||||||
|
});
|
||||||
res.json({ token: jwt });
|
res.json({ token: jwt });
|
||||||
})
|
});
|
||||||
export default JWTRoute;
|
export default JWTRoute;
|
38
Backend/src/api/oauth/profile.ts
Normal file
38
Backend/src/api/oauth/profile.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import Mail from "../../models/mail";
|
||||||
|
import { GetClientApiAuthMiddleware } from "../middlewares/client";
|
||||||
|
import Stacker from "../middlewares/stacker";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
|
||||||
|
export default Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => {
|
||||||
|
const mode = req.query.mode;
|
||||||
|
let mails = await Promise.all(
|
||||||
|
req.user.mails.map((id) => Mail.findById(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
let mail = mails.find((e) => e.primary) || mails[0];
|
||||||
|
|
||||||
|
let base_response = {
|
||||||
|
user_id: req.user.uid,
|
||||||
|
id: req.user.uid,
|
||||||
|
ID: req.user.uid,
|
||||||
|
sub: req.user.uid,
|
||||||
|
email: mail.mail,
|
||||||
|
username: req.user.username,
|
||||||
|
displayName: req.user.name,
|
||||||
|
displayNameClaim: req.user.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == "nextcloud") {
|
||||||
|
Logging.debug("Profile in Nextcloud mode");
|
||||||
|
base_response["ocs"] = {
|
||||||
|
data: {
|
||||||
|
id: base_response.user_id,
|
||||||
|
email: base_response.email,
|
||||||
|
"display-name": base_response.displayName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(base_response);
|
||||||
|
})
|
@ -2,5 +2,5 @@ import { Request, Response } from "express";
|
|||||||
import { public_key } from "../../keys";
|
import { public_key } from "../../keys";
|
||||||
|
|
||||||
export default function Public(req: Request, res: Response) {
|
export default function Public(req: Request, res: Response) {
|
||||||
res.json({ public_key: public_key })
|
res.json({ public_key: public_key });
|
||||||
}
|
}
|
122
Backend/src/api/oauth/refresh.ts
Normal file
122
Backend/src/api/oauth/refresh.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||||
|
import User from "../../models/user";
|
||||||
|
import Client from "../../models/client";
|
||||||
|
import {
|
||||||
|
getAccessTokenJWT,
|
||||||
|
getIDToken,
|
||||||
|
AccessTokenJWTExp,
|
||||||
|
} 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 { getEncryptionKey } from "../../helper/user_key";
|
||||||
|
import { refreshTokenValidTime } from "../../config";
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
/*
|
||||||
|
For example, the authorization server could employ refresh token
|
||||||
|
rotation in which a new refresh token is issued with every access
|
||||||
|
token refresh response. The previous refresh token is invalidated but retained by the authorization server. If a refresh token is
|
||||||
|
compromised and subsequently used by both the attacker and the
|
||||||
|
legitimate client, one of them will present an invalidated refresh
|
||||||
|
token, which will inform the authorization server of the breach.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RefreshTokenRoute = Stacker(
|
||||||
|
GetClientAuthMiddleware(false, false, true),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let grant_type = req.query.grant_type || req.body.grant_type;
|
||||||
|
if (!grant_type || grant_type === "authorization_code") {
|
||||||
|
let code = req.query.code || req.body.code;
|
||||||
|
let nonce = req.query.nonce || req.body.nonce;
|
||||||
|
|
||||||
|
let c = await ClientCode.findOne({ code: code });
|
||||||
|
if (!c || moment(c.validTill).isBefore()) {
|
||||||
|
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(refreshTokenValidTime).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 getAccessTokenJWT({
|
||||||
|
client: client,
|
||||||
|
user: user,
|
||||||
|
permissions: c.permissions,
|
||||||
|
}),
|
||||||
|
token_type: "bearer",
|
||||||
|
expires_in: AccessTokenJWTExp.asSeconds(),
|
||||||
|
profile: {
|
||||||
|
uid: user.uid,
|
||||||
|
email: mail ? mail.mail : "",
|
||||||
|
name: user.name,
|
||||||
|
enc_key: getEncryptionKey(user, client),
|
||||||
|
},
|
||||||
|
id_token: getIDToken(user, client.client_id, nonce),
|
||||||
|
});
|
||||||
|
} 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 || !token.valid || moment(token.validTill).isBefore())
|
||||||
|
throw new RequestError(
|
||||||
|
req.__("Invalid token"),
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
|
||||||
|
token.validTill = moment().add(refreshTokenValidTime).toDate();
|
||||||
|
await RefreshToken.save(token);
|
||||||
|
|
||||||
|
let user = await User.findById(token.user);
|
||||||
|
let client = await Client.findById(token.client);
|
||||||
|
let jwt = await getAccessTokenJWT({
|
||||||
|
user,
|
||||||
|
client,
|
||||||
|
permissions: token.permissions,
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
access_token: jwt,
|
||||||
|
expires_in: AccessTokenJWTExp.asSeconds(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new RequestError(
|
||||||
|
"invalid grant_type",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RefreshTokenRoute;
|
19
Backend/src/api/user/account.ts
Normal file
19
Backend/src/api/user/account.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../middlewares/user";
|
||||||
|
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||||
|
|
||||||
|
export const GetAccount = Stacker(
|
||||||
|
GetUserMiddleware(true, true),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let user = {
|
||||||
|
id: req.user.uid,
|
||||||
|
name: req.user.name,
|
||||||
|
username: req.user.username,
|
||||||
|
birthday: req.user.birthday,
|
||||||
|
gender: req.user.gender,
|
||||||
|
};
|
||||||
|
res.json({ user });
|
||||||
|
}
|
||||||
|
);
|
19
Backend/src/api/user/contact.ts
Normal file
19
Backend/src/api/user/contact.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../middlewares/user";
|
||||||
|
import Mail from "../../models/mail";
|
||||||
|
|
||||||
|
export const GetContactInfos = Stacker(
|
||||||
|
GetUserMiddleware(true, true),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let mails = await Promise.all(
|
||||||
|
req.user.mails.map((mail) => Mail.findById(mail))
|
||||||
|
);
|
||||||
|
|
||||||
|
let contact = {
|
||||||
|
mails: mails.filter((e) => !!e),
|
||||||
|
phones: req.user.phones,
|
||||||
|
};
|
||||||
|
res.json({ contact });
|
||||||
|
}
|
||||||
|
);
|
@ -1,129 +1,132 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import Register from "./register";
|
import { GetAccount } from "./account";
|
||||||
import Login from "./login";
|
import { GetContactInfos } from "./contact";
|
||||||
import TwoFactorRoute from "./twofactor";
|
import Login from "./login";
|
||||||
import { GetToken, DeleteToken } from "./token";
|
import Register from "./register";
|
||||||
import { GetAccount } from "./account";
|
import { DeleteToken, GetToken } from "./token";
|
||||||
import { GetContactInfos } from "./contact";
|
import TwoFactorRoute from "./twofactor";
|
||||||
|
import OAuthRoute from "./oauth";
|
||||||
const UserRoute: Router = Router();
|
|
||||||
|
const UserRoute: Router = Router();
|
||||||
/**
|
|
||||||
* @api {post} /user/register
|
/**
|
||||||
* @apiName UserRegister
|
* @api {post} /user/register
|
||||||
*
|
* @apiName UserRegister
|
||||||
* @apiGroup user
|
*
|
||||||
* @apiPermission none
|
* @apiGroup user
|
||||||
*
|
* @apiPermission none
|
||||||
* @apiParam {String} mail EMail linked to this Account
|
*
|
||||||
* @apiParam {String} username The new Username
|
* @apiParam {String} mail EMail linked to this Account
|
||||||
* @apiParam {String} password Password hashed and salted like specification
|
* @apiParam {String} username The new Username
|
||||||
* @apiParam {String} salt The Salt used for password hashing
|
* @apiParam {String} password Password hashed and salted like specification
|
||||||
* @apiParam {String} regcode The regcode, that should be used
|
* @apiParam {String} salt The Salt used for password hashing
|
||||||
* @apiParam {String} gender Gender can be: "male", "female", "other", "none"
|
* @apiParam {String} regcode The regcode, that should be used
|
||||||
* @apiParam {String} name The real name of the User
|
* @apiParam {String} gender Gender can be: "male", "female", "other", "none"
|
||||||
*
|
* @apiParam {String} name The real name of the User
|
||||||
* @apiSuccess {Boolean} success
|
*
|
||||||
*
|
* @apiSuccess {Boolean} success
|
||||||
* @apiErrorExample {Object} Error-Response:
|
*
|
||||||
{
|
* @apiErrorExample {Object} Error-Response:
|
||||||
error: [
|
{
|
||||||
{
|
error: [
|
||||||
message: "Some Error",
|
{
|
||||||
field: "username"
|
message: "Some Error",
|
||||||
}
|
field: "username"
|
||||||
],
|
}
|
||||||
status: 400
|
],
|
||||||
}
|
status: 400
|
||||||
*/
|
}
|
||||||
UserRoute.post("/register", Register);
|
*/
|
||||||
|
UserRoute.post("/register", Register);
|
||||||
/**
|
|
||||||
* @api {post} /user/login?type=:type
|
/**
|
||||||
* @apiName UserLogin
|
* @api {post} /user/login?type=:type
|
||||||
*
|
* @apiName UserLogin
|
||||||
* @apiParam {String} type Type could be either "username" or "password"
|
*
|
||||||
*
|
* @apiParam {String} type Type could be either "username" or "password"
|
||||||
* @apiGroup user
|
*
|
||||||
* @apiPermission none
|
* @apiGroup user
|
||||||
*
|
* @apiPermission none
|
||||||
* @apiParam {String} username Username (either username or uid required)
|
*
|
||||||
* @apiParam {String} uid (either username or uid required)
|
* @apiParam {String} username Username (either username or uid required)
|
||||||
* @apiParam {String} password Password hashed and salted like specification (only on type password)
|
* @apiParam {String} uid (either username or uid required)
|
||||||
* @apiParam {Number} time in milliseconds used to hash password. This is used to make passwords "expire"
|
* @apiParam {String} password Password hashed and salted like specification (only on type password)
|
||||||
*
|
* @apiParam {Number} time in milliseconds used to hash password. This is used to make passwords "expire"
|
||||||
* @apiSuccess {String} uid On type = "username"
|
*
|
||||||
* @apiSuccess {String} salt On type = "username"
|
* @apiSuccess {String} uid On type = "username"
|
||||||
*
|
* @apiSuccess {String} salt On type = "username"
|
||||||
* @apiSuccess {String} login On type = "password". Login Token
|
*
|
||||||
* @apiSuccess {String} special On type = "password". Special Token
|
* @apiSuccess {String} login On type = "password". Login Token
|
||||||
* @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required
|
* @apiSuccess {String} special On type = "password". Special Token
|
||||||
* @apiSuccess {String} tfa.id The ID of the TFA Method
|
* @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required
|
||||||
* @apiSuccess {String} tfa.name The name of the TFA Method
|
* @apiSuccess {String} tfa.id The ID of the TFA Method
|
||||||
* @apiSuccess {String} tfa.type The type of the TFA Method
|
* @apiSuccess {String} tfa.name The name of the TFA Method
|
||||||
*/
|
* @apiSuccess {String} tfa.type The type of the TFA Method
|
||||||
UserRoute.post("/login", Login)
|
*/
|
||||||
UserRoute.use("/twofactor", TwoFactorRoute);
|
UserRoute.post("/login", Login);
|
||||||
|
UserRoute.use("/twofactor", TwoFactorRoute);
|
||||||
/**
|
|
||||||
* @api {get} /user/token
|
/**
|
||||||
* @apiName UserGetToken
|
* @api {get} /user/token
|
||||||
*
|
* @apiName UserGetToken
|
||||||
* @apiGroup user
|
*
|
||||||
* @apiPermission user
|
* @apiGroup user
|
||||||
*
|
* @apiPermission user
|
||||||
* @apiSuccess {Object[]} token
|
*
|
||||||
* @apiSuccess {String} token.id The Token ID
|
* @apiSuccess {Object[]} token
|
||||||
* @apiSuccess {String} token.special Identifies Special Token
|
* @apiSuccess {String} token.id The Token ID
|
||||||
* @apiSuccess {String} token.ip IP the token was optained from
|
* @apiSuccess {String} token.special Identifies Special Token
|
||||||
* @apiSuccess {String} token.browser The Browser the token was optained from (User Agent)
|
* @apiSuccess {String} token.ip IP the token was optained from
|
||||||
* @apiSuccess {Boolean} token.isthis Shows if it is token used by this session
|
* @apiSuccess {String} token.browser The Browser the token was optained from (User Agent)
|
||||||
*/
|
* @apiSuccess {Boolean} token.isthis Shows if it is token used by this session
|
||||||
UserRoute.get("/token", GetToken);
|
*/
|
||||||
|
UserRoute.get("/token", GetToken);
|
||||||
/**
|
|
||||||
* @api {delete} /user/token/:id
|
/**
|
||||||
* @apiParam {String} id The id of the token to be deleted
|
* @api {delete} /user/token/:id
|
||||||
*
|
* @apiParam {String} id The id of the token to be deleted
|
||||||
* @apiName UserDeleteToken
|
*
|
||||||
*
|
* @apiName UserDeleteToken
|
||||||
*
|
*
|
||||||
* @apiGroup user
|
*
|
||||||
* @apiPermission user
|
* @apiGroup user
|
||||||
*
|
* @apiPermission user
|
||||||
* @apiSuccess {Boolean} success
|
*
|
||||||
*/
|
* @apiSuccess {Boolean} success
|
||||||
UserRoute.delete("/token/:id", DeleteToken);
|
*/
|
||||||
|
UserRoute.delete("/token/:id", DeleteToken);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {delete} /user/account
|
* @api {delete} /user/account
|
||||||
* @apiName UserGetAccount
|
* @apiName UserGetAccount
|
||||||
*
|
*
|
||||||
* @apiGroup user
|
* @apiGroup user
|
||||||
* @apiPermission user
|
* @apiPermission user
|
||||||
*
|
*
|
||||||
* @apiSuccess {Boolean} success
|
* @apiSuccess {Boolean} success
|
||||||
* @apiSuccess {Object[]} user
|
* @apiSuccess {Object[]} user
|
||||||
* @apiSuccess {String} user.id User ID
|
* @apiSuccess {String} user.id User ID
|
||||||
* @apiSuccess {String} user.name Full name of the user
|
* @apiSuccess {String} user.name Full name of the user
|
||||||
* @apiSuccess {String} user.username Username of user
|
* @apiSuccess {String} user.username Username of user
|
||||||
* @apiSuccess {Date} user.birthday Birthday
|
* @apiSuccess {Date} user.birthday Birthday
|
||||||
* @apiSuccess {Number} user.gender Gender of user (none = 0, male = 1, female = 2, other = 3)
|
* @apiSuccess {Number} user.gender Gender of user (none = 0, male = 1, female = 2, other = 3)
|
||||||
*/
|
*/
|
||||||
UserRoute.get("/account", GetAccount);
|
UserRoute.get("/account", GetAccount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {delete} /user/account
|
* @api {delete} /user/account
|
||||||
* @apiName UserGetAccount
|
* @apiName UserGetAccount
|
||||||
*
|
*
|
||||||
* @apiGroup user
|
* @apiGroup user
|
||||||
* @apiPermission user
|
* @apiPermission user
|
||||||
*
|
*
|
||||||
* @apiSuccess {Boolean} success
|
* @apiSuccess {Boolean} success
|
||||||
* @apiSuccess {Object} contact
|
* @apiSuccess {Object} contact
|
||||||
* @apiSuccess {Object[]} user.mail EMail addresses
|
* @apiSuccess {Object[]} user.mail EMail addresses
|
||||||
* @apiSuccess {Object[]} user.phone Phone numbers
|
* @apiSuccess {Object[]} user.phone Phone numbers
|
||||||
*/
|
*/
|
||||||
UserRoute.get("/contact", GetContactInfos);
|
UserRoute.get("/contact", GetContactInfos);
|
||||||
export default UserRoute;
|
|
||||||
|
UserRoute.use("/oauth", OAuthRoute);
|
||||||
|
|
||||||
|
export default UserRoute;
|
@ -1,107 +1,134 @@
|
|||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express";
|
||||||
import User, { IUser } from "../../models/user";
|
import User, { IUser } from "../../models/user";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import moment = require("moment");
|
import moment = require("moment");
|
||||||
import LoginToken from "../../models/login_token";
|
import LoginToken from "../../models/login_token";
|
||||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||||
import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
|
import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import Logging from "@hibas123/nodelogging";
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
|
||||||
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||||
let type = req.query.type;
|
let type = req.query.type as string;
|
||||||
if (type === "username") {
|
if (type === "username") {
|
||||||
let { username, uid } = req.query;
|
let { username, uid } = req.query as { [key: string]: string };
|
||||||
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid });
|
let user = await User.findOne(
|
||||||
if (!user) {
|
username ? { username: username.toLowerCase() } : { uid: uid }
|
||||||
res.json({ error: req.__("User not found") })
|
);
|
||||||
} else {
|
if (!user) {
|
||||||
res.json({ salt: user.salt, uid: user.uid });
|
res.json({ error: req.__("User not found") });
|
||||||
}
|
} else {
|
||||||
return;
|
res.json({ salt: user.salt, uid: user.uid });
|
||||||
} else if (type === "password") {
|
}
|
||||||
const sendToken = async (user: IUser, tfa?: any[]) => {
|
return;
|
||||||
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
|
} else if (type === "password") {
|
||||||
let client = {
|
const sendToken = async (user: IUser, tfa?: any[]) => {
|
||||||
ip: Array.isArray(ip) ? ip[0] : ip,
|
let ip =
|
||||||
browser: req.headers["user-agent"]
|
req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
||||||
}
|
let client = {
|
||||||
|
ip: Array.isArray(ip) ? ip[0] : ip,
|
||||||
let token_str = randomBytes(16).toString("hex");
|
browser: req.headers["user-agent"],
|
||||||
let tfa_exp = moment().add(5, "minutes").toDate()
|
};
|
||||||
let token_exp = moment().add(6, "months").toDate()
|
|
||||||
let token = LoginToken.new({
|
let token_str = randomBytes(16).toString("hex");
|
||||||
token: token_str,
|
let tfa_exp = moment().add(5, "minutes").toDate();
|
||||||
valid: true,
|
let token_exp = moment().add(6, "months").toDate();
|
||||||
validTill: tfa ? tfa_exp : token_exp,
|
let token = LoginToken.new({
|
||||||
user: user._id,
|
token: token_str,
|
||||||
validated: tfa ? false : true,
|
valid: true,
|
||||||
...client
|
validTill: tfa ? tfa_exp : token_exp,
|
||||||
});
|
user: user._id,
|
||||||
await LoginToken.save(token);
|
validated: tfa ? false : true,
|
||||||
|
...client,
|
||||||
let special_str = randomBytes(24).toString("hex");
|
});
|
||||||
let special_exp = moment().add(30, "minutes").toDate()
|
await LoginToken.save(token);
|
||||||
let special = LoginToken.new({
|
|
||||||
token: special_str,
|
let special_str = randomBytes(24).toString("hex");
|
||||||
valid: true,
|
let special_exp = moment().add(30, "minutes").toDate();
|
||||||
validTill: tfa ? tfa_exp : special_exp,
|
let special = LoginToken.new({
|
||||||
special: true,
|
token: special_str,
|
||||||
user: user._id,
|
valid: true,
|
||||||
validated: tfa ? false : true,
|
validTill: tfa ? tfa_exp : special_exp,
|
||||||
...client
|
special: true,
|
||||||
});
|
user: user._id,
|
||||||
await LoginToken.save(special);
|
validated: tfa ? false : true,
|
||||||
|
...client,
|
||||||
res.json({
|
});
|
||||||
login: { token: token_str, expires: token.validTill.toUTCString() },
|
await LoginToken.save(special);
|
||||||
special: { token: special_str, expires: special.validTill.toUTCString() },
|
|
||||||
tfa
|
res.json({
|
||||||
});
|
login: { token: token_str, expires: token.validTill.toUTCString() },
|
||||||
}
|
special: {
|
||||||
|
token: special_str,
|
||||||
let { username, password, uid, date } = req.body;
|
expires: special.validTill.toUTCString(),
|
||||||
|
},
|
||||||
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
|
tfa,
|
||||||
if (!user) {
|
});
|
||||||
res.json({ error: req.__("User not found") })
|
};
|
||||||
} else {
|
|
||||||
let upw = user.password;
|
let { username, password, uid, date } = req.body;
|
||||||
if (date) {
|
|
||||||
if (!moment(date).isBetween(moment().subtract(1, "minute"), moment().add(1, "minute"))) {
|
let user = await User.findOne(
|
||||||
res.json({ error: req.__("Invalid timestamp. Please check your devices time!") });
|
username ? { username: username.toLowerCase() } : { uid: uid }
|
||||||
return;
|
);
|
||||||
} else {
|
if (!user) {
|
||||||
upw = crypto.createHash("sha512").update(upw + date.toString()).digest("hex");
|
res.json({ error: req.__("User not found") });
|
||||||
}
|
} else {
|
||||||
}
|
let upw = user.password;
|
||||||
if (upw !== password) {
|
if (date) {
|
||||||
res.json({ error: req.__("Password or username wrong") })
|
if (
|
||||||
} else {
|
!moment(date).isBetween(
|
||||||
let twofactor = await TwoFactor.find({ user: user._id, valid: true })
|
moment().subtract(1, "minute"),
|
||||||
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
|
moment().add(1, "minute")
|
||||||
await Promise.all(expired.map(e => {
|
)
|
||||||
e.valid = false;
|
) {
|
||||||
return TwoFactor.save(e);
|
res.json({
|
||||||
}));
|
error: req.__(
|
||||||
twofactor = twofactor.filter(e => e.valid);
|
"Invalid timestamp. Please check your devices time!"
|
||||||
if (twofactor && twofactor.length > 0) {
|
),
|
||||||
let tfa = twofactor.map(e => {
|
});
|
||||||
return {
|
return;
|
||||||
id: e._id,
|
} else {
|
||||||
name: e.name || TFANames.get(e.type),
|
upw = crypto
|
||||||
type: e.type
|
.createHash("sha512")
|
||||||
}
|
.update(upw + date.toString())
|
||||||
})
|
.digest("hex");
|
||||||
await sendToken(user, tfa);
|
}
|
||||||
} else {
|
}
|
||||||
await sendToken(user);
|
if (upw !== password) {
|
||||||
}
|
res.json({ error: req.__("Password or username wrong") });
|
||||||
}
|
} else {
|
||||||
}
|
let twofactor = await TwoFactor.find({
|
||||||
} else {
|
user: user._id,
|
||||||
res.json({ error: req.__("Invalid type!") });
|
valid: true,
|
||||||
}
|
});
|
||||||
});
|
let expired = twofactor.filter((e) =>
|
||||||
|
e.expires ? moment().isAfter(moment(e.expires)) : false
|
||||||
export default Login;
|
);
|
||||||
|
await Promise.all(
|
||||||
|
expired.map((e) => {
|
||||||
|
e.valid = false;
|
||||||
|
return TwoFactor.save(e);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
twofactor = twofactor.filter((e) => e.valid);
|
||||||
|
if (twofactor && twofactor.length > 0) {
|
||||||
|
let tfa = twofactor.map((e) => {
|
||||||
|
return {
|
||||||
|
id: e._id,
|
||||||
|
name: e.name || TFANames.get(e.type),
|
||||||
|
type: e.type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await sendToken(user, tfa);
|
||||||
|
} else {
|
||||||
|
await sendToken(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.json({ error: req.__("Invalid type!") });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Login;
|
21
Backend/src/api/user/oauth/_helper.ts
Normal file
21
Backend/src/api/user/oauth/_helper.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||||
|
import Client, { IClient } from "../../../models/client";
|
||||||
|
|
||||||
|
export async function getClientWithOrigin(client_id: string, origin: string) {
|
||||||
|
const client = await Client.findOne({
|
||||||
|
client_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientNotFoundError = new RequestError(
|
||||||
|
"Client not found!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!client) throw clientNotFoundError;
|
||||||
|
|
||||||
|
const clientUrl = new URL(client.redirect_url);
|
||||||
|
|
||||||
|
if (clientUrl.hostname !== origin) throw clientNotFoundError;
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
12
Backend/src/api/user/oauth/index.ts
Normal file
12
Backend/src/api/user/oauth/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { GetJWTByUser } from "./jwt";
|
||||||
|
import { GetPermissionsForAuthRequest } from "./permissions";
|
||||||
|
import { GetTokenByUser } from "./refresh_token";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/jwt", GetJWTByUser);
|
||||||
|
router.get("/permissions", GetPermissionsForAuthRequest);
|
||||||
|
router.get("/refresh_token", GetTokenByUser);
|
||||||
|
|
||||||
|
export default router;
|
25
Backend/src/api/user/oauth/jwt.ts
Normal file
25
Backend/src/api/user/oauth/jwt.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../../middlewares/user";
|
||||||
|
import { URL } from "url";
|
||||||
|
import Client from "../../../models/client";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||||
|
import { getAccessTokenJWT } from "../../../helper/jwt";
|
||||||
|
import { getClientWithOrigin } from "./_helper";
|
||||||
|
|
||||||
|
export const GetJWTByUser = Stacker(
|
||||||
|
GetUserMiddleware(true, false),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const { client_id, origin } = req.query as { [key: string]: string };
|
||||||
|
|
||||||
|
const client = await getClientWithOrigin(client_id, origin);
|
||||||
|
|
||||||
|
const jwt = await getAccessTokenJWT({
|
||||||
|
user: req.user,
|
||||||
|
client: client,
|
||||||
|
permissions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ jwt });
|
||||||
|
}
|
||||||
|
);
|
38
Backend/src/api/user/oauth/permissions.ts
Normal file
38
Backend/src/api/user/oauth/permissions.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../../middlewares/user";
|
||||||
|
import { URL } from "url";
|
||||||
|
import Client from "../../../models/client";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import moment = require("moment");
|
||||||
|
import RefreshToken from "../../../models/refresh_token";
|
||||||
|
import { refreshTokenValidTime } from "../../../config";
|
||||||
|
import { getClientWithOrigin } from "./_helper";
|
||||||
|
import Permission from "../../../models/permissions";
|
||||||
|
|
||||||
|
export const GetPermissionsForAuthRequest = Stacker(
|
||||||
|
GetUserMiddleware(true, false),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const { client_id, origin, permissions } = req.query as {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await getClientWithOrigin(client_id, origin);
|
||||||
|
|
||||||
|
const perm = permissions.split(",").filter((e) => !!e);
|
||||||
|
|
||||||
|
const resolved = await Promise.all(
|
||||||
|
perm.map((p) => Permission.findById(p))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resolved.some((e) => e.grant_type !== "user")) {
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Permission requested",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ permissions: resolved });
|
||||||
|
}
|
||||||
|
);
|
49
Backend/src/api/user/oauth/refresh_token.ts
Normal file
49
Backend/src/api/user/oauth/refresh_token.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../../middlewares/user";
|
||||||
|
import { URL } from "url";
|
||||||
|
import Client from "../../../models/client";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import moment = require("moment");
|
||||||
|
import RefreshToken from "../../../models/refresh_token";
|
||||||
|
import { refreshTokenValidTime } from "../../../config";
|
||||||
|
import { getClientWithOrigin } from "./_helper";
|
||||||
|
import Permission from "../../../models/permissions";
|
||||||
|
|
||||||
|
export const GetTokenByUser = Stacker(
|
||||||
|
GetUserMiddleware(true, false),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const { client_id, origin, permissions } = req.query as {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await getClientWithOrigin(client_id, origin);
|
||||||
|
|
||||||
|
const perm = permissions.split(",").filter((e) => !!e);
|
||||||
|
|
||||||
|
const resolved = await Promise.all(
|
||||||
|
perm.map((p) => Permission.findById(p))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resolved.some((e) => e.grant_type !== "user")) {
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Permission requested",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = RefreshToken.new({
|
||||||
|
user: req.user._id,
|
||||||
|
client: client._id,
|
||||||
|
permissions: resolved.map((e) => e._id),
|
||||||
|
token: randomBytes(16).toString("hex"),
|
||||||
|
valid: true,
|
||||||
|
validTill: moment().add(refreshTokenValidTime).toDate(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await RefreshToken.save(token);
|
||||||
|
|
||||||
|
res.json({ token });
|
||||||
|
}
|
||||||
|
);
|
155
Backend/src/api/user/register.ts
Normal file
155
Backend/src/api/user/register.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Mail.save(ml);
|
||||||
|
|
||||||
|
user.mails.push(ml._id);
|
||||||
|
await User.save(user);
|
||||||
|
res.json({ success: true });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export default Register;
|
45
Backend/src/api/user/token.ts
Normal file
45
Backend/src/api/user/token.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../middlewares/user";
|
||||||
|
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||||
|
|
||||||
|
export const GetToken = Stacker(
|
||||||
|
GetUserMiddleware(true, true),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let raw_token = await LoginToken.find({
|
||||||
|
user: req.user._id,
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
let token = await Promise.all(
|
||||||
|
raw_token
|
||||||
|
.map(async (token) => {
|
||||||
|
await CheckToken(token);
|
||||||
|
return {
|
||||||
|
id: token._id,
|
||||||
|
special: token.special,
|
||||||
|
ip: token.ip,
|
||||||
|
browser: token.browser,
|
||||||
|
isthis: token._id.equals(
|
||||||
|
token.special ? req.token.special._id : req.token.login._id
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((t) => t !== undefined)
|
||||||
|
);
|
||||||
|
res.json({ token });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DeleteToken = Stacker(
|
||||||
|
GetUserMiddleware(true, true),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let { id } = req.params;
|
||||||
|
let token = await LoginToken.findById(id);
|
||||||
|
if (!token || !token.user.equals(req.user._id))
|
||||||
|
throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
|
||||||
|
token.valid = false;
|
||||||
|
await LoginToken.save(token);
|
||||||
|
res.json({ success: true });
|
||||||
|
}
|
||||||
|
);
|
100
Backend/src/api/user/twofactor/backup/index.ts
Normal file
100
Backend/src/api/user/twofactor/backup/index.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import Stacker from "../../../middlewares/stacker";
|
||||||
|
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||||
|
import TwoFactor, {
|
||||||
|
TFATypes as TwoFATypes,
|
||||||
|
IBackupCode,
|
||||||
|
} from "../../../../models/twofactor";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||||
|
import moment = require("moment");
|
||||||
|
import { upgradeToken } from "../helper";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
|
||||||
|
const BackupCodeRoute = Router();
|
||||||
|
|
||||||
|
// TODO: Further checks if this is good enough randomness
|
||||||
|
function generateCode(length: number) {
|
||||||
|
let bytes = crypto.randomBytes(length);
|
||||||
|
let nrs = "";
|
||||||
|
bytes.forEach((b, idx) => {
|
||||||
|
let nr = Math.floor((b / 255) * 9.9999);
|
||||||
|
if (nr > 9) nr = 9;
|
||||||
|
nrs += String(nr);
|
||||||
|
});
|
||||||
|
return nrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupCodeRoute.post(
|
||||||
|
"/",
|
||||||
|
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||||
|
//Generating new
|
||||||
|
let codes = Array(10).map(() => generateCode(8));
|
||||||
|
console.log(codes);
|
||||||
|
let twofactor = TwoFactor.new(<IBackupCode>{
|
||||||
|
user: req.user._id,
|
||||||
|
type: TwoFATypes.OTC,
|
||||||
|
valid: true,
|
||||||
|
data: codes,
|
||||||
|
name: "",
|
||||||
|
});
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
res.json({
|
||||||
|
codes,
|
||||||
|
id: twofactor._id,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
BackupCodeRoute.put(
|
||||||
|
"/",
|
||||||
|
Stacker(
|
||||||
|
GetUserMiddleware(true, false, undefined, false),
|
||||||
|
async (req, res) => {
|
||||||
|
let { login, special } = req.token;
|
||||||
|
let { id, code }: { id: string; code: string } = req.body;
|
||||||
|
|
||||||
|
let twofactor: IBackupCode = await TwoFactor.findById(id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!twofactor ||
|
||||||
|
!twofactor.valid ||
|
||||||
|
!twofactor.user.equals(req.user._id) ||
|
||||||
|
twofactor.type !== TwoFATypes.OTC
|
||||||
|
) {
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Method!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||||
|
twofactor.valid = false;
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Method!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
code = code.replace(/\s/g, "");
|
||||||
|
let valid = twofactor.data.find((c) => c === code);
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
twofactor.data = twofactor.data.filter((c) => c !== code);
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
let [login_exp, special_exp] = await Promise.all([
|
||||||
|
upgradeToken(login),
|
||||||
|
upgradeToken(special),
|
||||||
|
]);
|
||||||
|
res.json({ success: true, login_exp, special_exp });
|
||||||
|
} else {
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid or already used code!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BackupCodeRoute;
|
@ -1,13 +1,16 @@
|
|||||||
import LoginToken, { ILoginToken } from "../../../models/login_token";
|
import LoginToken, { ILoginToken } from "../../../models/login_token";
|
||||||
import moment = require("moment");
|
import moment = require("moment");
|
||||||
|
|
||||||
export async function upgradeToken(token: ILoginToken) {
|
export async function upgradeToken(token: ILoginToken) {
|
||||||
token.data = undefined;
|
token.data = undefined;
|
||||||
token.valid = true;
|
token.valid = true;
|
||||||
token.validated = true;
|
token.validated = true;
|
||||||
//TODO durations from config
|
//TODO durations from config
|
||||||
let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate();
|
let expires = (token.special
|
||||||
token.validTill = expires;
|
? moment().add(30, "minute")
|
||||||
await LoginToken.save(token);
|
: moment().add(6, "months")
|
||||||
return expires;
|
).toDate();
|
||||||
}
|
token.validTill = expires;
|
||||||
|
await LoginToken.save(token);
|
||||||
|
return expires;
|
||||||
|
}
|
56
Backend/src/api/user/twofactor/index.ts
Normal file
56
Backend/src/api/user/twofactor/index.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import YubiKeyRoute from "./yubikey";
|
||||||
|
import { GetUserMiddleware } from "../../middlewares/user";
|
||||||
|
import Stacker from "../../middlewares/stacker";
|
||||||
|
import TwoFactor from "../../../models/twofactor";
|
||||||
|
import * as moment from "moment";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||||
|
import OTCRoute from "./otc";
|
||||||
|
import BackupCodeRoute from "./backup";
|
||||||
|
|
||||||
|
const TwoFactorRouter = Router();
|
||||||
|
|
||||||
|
TwoFactorRouter.get(
|
||||||
|
"/",
|
||||||
|
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||||
|
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true });
|
||||||
|
let expired = twofactor.filter((e) =>
|
||||||
|
e.expires ? moment().isAfter(moment(e.expires)) : false
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
expired.map((e) => {
|
||||||
|
e.valid = false;
|
||||||
|
return TwoFactor.save(e);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
twofactor = twofactor.filter((e) => e.valid);
|
||||||
|
let tfa = twofactor.map((e) => {
|
||||||
|
return {
|
||||||
|
id: e._id,
|
||||||
|
name: e.name,
|
||||||
|
type: e.type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
res.json({ methods: tfa });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
TwoFactorRouter.delete(
|
||||||
|
"/:id",
|
||||||
|
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||||
|
let { id } = req.params;
|
||||||
|
let tfa = await TwoFactor.findById(id);
|
||||||
|
if (!tfa || !tfa.user.equals(req.user._id)) {
|
||||||
|
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
tfa.valid = false;
|
||||||
|
await TwoFactor.save(tfa);
|
||||||
|
res.json({ success: true });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
|
||||||
|
TwoFactorRouter.use("/otc", OTCRoute);
|
||||||
|
TwoFactorRouter.use("/backup", BackupCodeRoute);
|
||||||
|
|
||||||
|
export default TwoFactorRouter;
|
@ -1,140 +1,135 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import Stacker from "../../../middlewares/stacker";
|
import Stacker from "../../../middlewares/stacker";
|
||||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||||
import TwoFactor, {
|
import TwoFactor, {
|
||||||
TFATypes as TwoFATypes,
|
TFATypes as TwoFATypes,
|
||||||
IOTC,
|
IOTC,
|
||||||
} from "../../../../models/twofactor";
|
} from "../../../../models/twofactor";
|
||||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||||
import moment = require("moment");
|
import moment = require("moment");
|
||||||
import { upgradeToken } from "../helper";
|
import { upgradeToken } from "../helper";
|
||||||
import Logging from "@hibas123/nodelogging";
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
|
||||||
import * as speakeasy from "speakeasy";
|
import * as speakeasy from "speakeasy";
|
||||||
import * as qrcode from "qrcode";
|
import * as qrcode from "qrcode";
|
||||||
import config from "../../../../config";
|
import config from "../../../../config";
|
||||||
|
|
||||||
const OTCRoute = Router();
|
const OTCRoute = Router();
|
||||||
|
|
||||||
OTCRoute.post(
|
OTCRoute.post(
|
||||||
"/",
|
"/",
|
||||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||||
const { type } = req.query;
|
const { type } = req.query;
|
||||||
if (type === "create") {
|
if (type === "create") {
|
||||||
//Generating new
|
//Generating new
|
||||||
let secret = speakeasy.generateSecret({
|
let secret = speakeasy.generateSecret({
|
||||||
name: config.core.name,
|
name: config.core.name,
|
||||||
issuer: config.core.name,
|
issuer: config.core.name,
|
||||||
});
|
});
|
||||||
let twofactor = TwoFactor.new(<IOTC>{
|
let twofactor = TwoFactor.new(<IOTC>{
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
type: TwoFATypes.OTC,
|
type: TwoFATypes.OTC,
|
||||||
valid: false,
|
valid: false,
|
||||||
data: secret.base32,
|
data: secret.base32,
|
||||||
});
|
});
|
||||||
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
||||||
await TwoFactor.save(twofactor);
|
await TwoFactor.save(twofactor);
|
||||||
res.json({
|
res.json({
|
||||||
image: dataurl,
|
image: dataurl,
|
||||||
id: twofactor._id,
|
id: twofactor._id,
|
||||||
});
|
});
|
||||||
} else if (type === "validate") {
|
} else if (type === "validate") {
|
||||||
// Checking code and marking as valid
|
// Checking code and marking as valid
|
||||||
const { code, id } = req.body;
|
const { code, id } = req.body;
|
||||||
Logging.debug(req.body, id);
|
Logging.debug(req.body, id);
|
||||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||||
const err = () => {
|
const err = () => {
|
||||||
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
!twofactor ||
|
!twofactor ||
|
||||||
!twofactor.user.equals(req.user._id) ||
|
!twofactor.user.equals(req.user._id) ||
|
||||||
twofactor.type !== TwoFATypes.OTC ||
|
twofactor.type !== TwoFATypes.OTC ||
|
||||||
!twofactor.data ||
|
!twofactor.data ||
|
||||||
twofactor.valid
|
twofactor.valid
|
||||||
) {
|
) {
|
||||||
Logging.debug("Not found or wrong user", twofactor);
|
Logging.debug("Not found or wrong user", twofactor);
|
||||||
err();
|
err();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
||||||
await TwoFactor.delete(twofactor);
|
await TwoFactor.delete(twofactor);
|
||||||
Logging.debug("Expired!", twofactor);
|
Logging.debug("Expired!", twofactor);
|
||||||
err();
|
err();
|
||||||
}
|
}
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
let valid = speakeasy.totp.verify({
|
||||||
secret: twofactor.data,
|
secret: twofactor.data,
|
||||||
encoding: "base32",
|
encoding: "base32",
|
||||||
token: code,
|
token: code,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
twofactor.expires = undefined;
|
twofactor.expires = undefined;
|
||||||
twofactor.valid = true;
|
twofactor.valid = true;
|
||||||
await TwoFactor.save(twofactor);
|
await TwoFactor.save(twofactor);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} else {
|
} else {
|
||||||
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
|
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
|
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
OTCRoute.put(
|
OTCRoute.put(
|
||||||
"/",
|
"/",
|
||||||
Stacker(
|
Stacker(
|
||||||
GetUserMiddleware(true, false, undefined, false),
|
GetUserMiddleware(true, false, undefined, false),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
let { login, special } = req.token;
|
let { login, special } = req.token;
|
||||||
let { id, code } = req.body;
|
let { id, code } = req.body;
|
||||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!twofactor ||
|
!twofactor ||
|
||||||
!twofactor.valid ||
|
!twofactor.valid ||
|
||||||
!twofactor.user.equals(req.user._id) ||
|
!twofactor.user.equals(req.user._id) ||
|
||||||
twofactor.type !== TwoFATypes.OTC
|
twofactor.type !== TwoFATypes.OTC
|
||||||
) {
|
) {
|
||||||
throw new RequestError(
|
throw new RequestError(
|
||||||
"Invalid Method!",
|
"Invalid Method!",
|
||||||
HttpStatusCode.BAD_REQUEST
|
HttpStatusCode.BAD_REQUEST
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||||
twofactor.valid = false;
|
twofactor.valid = false;
|
||||||
await TwoFactor.save(twofactor);
|
await TwoFactor.save(twofactor);
|
||||||
throw new RequestError(
|
throw new RequestError(
|
||||||
"Invalid Method!",
|
"Invalid Method!",
|
||||||
HttpStatusCode.BAD_REQUEST
|
HttpStatusCode.BAD_REQUEST
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
let valid = speakeasy.totp.verify({
|
||||||
secret: twofactor.data,
|
secret: twofactor.data,
|
||||||
encoding: "base32",
|
encoding: "base32",
|
||||||
token: code,
|
token: code,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!valid && config.core.dev === true && code === "000000") {
|
if (valid) {
|
||||||
Logging.warning("Dev mode enabled and dev OTC used.");
|
let [login_exp, special_exp] = await Promise.all([
|
||||||
valid = true;
|
upgradeToken(login),
|
||||||
}
|
upgradeToken(special),
|
||||||
|
]);
|
||||||
if (valid) {
|
res.json({ success: true, login_exp, special_exp });
|
||||||
let [login_exp, special_exp] = await Promise.all([
|
} else {
|
||||||
upgradeToken(login),
|
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
|
||||||
upgradeToken(special),
|
}
|
||||||
]);
|
}
|
||||||
res.json({ success: true, login_exp, special_exp });
|
)
|
||||||
} else {
|
);
|
||||||
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
|
|
||||||
}
|
export default OTCRoute;
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default OTCRoute;
|
|
206
Backend/src/api/user/twofactor/yubikey/index.ts
Normal file
206
Backend/src/api/user/twofactor/yubikey/index.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { Router, Request } from "express";
|
||||||
|
import Stacker from "../../../middlewares/stacker";
|
||||||
|
import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user";
|
||||||
|
import * as u2f from "u2f";
|
||||||
|
import config from "../../../../config";
|
||||||
|
import TwoFactor, {
|
||||||
|
TFATypes as TwoFATypes,
|
||||||
|
IYubiKey,
|
||||||
|
} from "../../../../models/twofactor";
|
||||||
|
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||||
|
import moment = require("moment");
|
||||||
|
import LoginToken from "../../../../models/login_token";
|
||||||
|
import { upgradeToken } from "../helper";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
|
||||||
|
const U2FRoute = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registerinf a new YubiKey
|
||||||
|
*/
|
||||||
|
U2FRoute.post(
|
||||||
|
"/",
|
||||||
|
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||||
|
const { type } = req.query;
|
||||||
|
if (type === "challenge") {
|
||||||
|
const registrationRequest = u2f.request(config.core.url);
|
||||||
|
|
||||||
|
let twofactor = TwoFactor.new(<IYubiKey>{
|
||||||
|
user: req.user._id,
|
||||||
|
type: TwoFATypes.U2F,
|
||||||
|
valid: false,
|
||||||
|
data: {
|
||||||
|
registration: registrationRequest,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
res.json({
|
||||||
|
request: registrationRequest,
|
||||||
|
id: twofactor._id,
|
||||||
|
appid: config.core.url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { response, id } = req.body;
|
||||||
|
Logging.debug(req.body, id);
|
||||||
|
let twofactor: IYubiKey = await TwoFactor.findById(id);
|
||||||
|
const err = () => {
|
||||||
|
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
!twofactor ||
|
||||||
|
!twofactor.user.equals(req.user._id) ||
|
||||||
|
twofactor.type !== TwoFATypes.U2F ||
|
||||||
|
!twofactor.data.registration ||
|
||||||
|
twofactor.valid
|
||||||
|
) {
|
||||||
|
Logging.debug("Not found or wrong user", twofactor);
|
||||||
|
err();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
||||||
|
await TwoFactor.delete(twofactor);
|
||||||
|
Logging.debug("Expired!", twofactor);
|
||||||
|
err();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = u2f.checkRegistration(
|
||||||
|
twofactor.data.registration,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.successful) {
|
||||||
|
twofactor.data = {
|
||||||
|
keyHandle: result.keyHandle,
|
||||||
|
publicKey: result.publicKey,
|
||||||
|
};
|
||||||
|
twofactor.expires = undefined;
|
||||||
|
twofactor.valid = true;
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
throw new RequestError(
|
||||||
|
result.errorMessage,
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
U2FRoute.get(
|
||||||
|
"/",
|
||||||
|
Stacker(
|
||||||
|
GetUserMiddleware(true, false, undefined, false),
|
||||||
|
async (req, res) => {
|
||||||
|
let { login, special } = req.token;
|
||||||
|
let twofactor: IYubiKey = await TwoFactor.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
type: TwoFATypes.U2F,
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!twofactor) {
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Method!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twofactor.expires) {
|
||||||
|
if (moment().isAfter(twofactor.expires)) {
|
||||||
|
twofactor.valid = false;
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Method!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = u2f.request(config.core.url, twofactor.data.keyHandle);
|
||||||
|
login.data = {
|
||||||
|
type: "ykr",
|
||||||
|
request,
|
||||||
|
};
|
||||||
|
let r;
|
||||||
|
if (special) {
|
||||||
|
special.data = login.data;
|
||||||
|
r = LoginToken.save(special);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([r, LoginToken.save(login)]);
|
||||||
|
res.json({ request });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
U2FRoute.put(
|
||||||
|
"/",
|
||||||
|
Stacker(
|
||||||
|
GetUserMiddleware(true, false, undefined, false),
|
||||||
|
async (req, res) => {
|
||||||
|
let { login, special } = req.token;
|
||||||
|
let twofactor: IYubiKey = await TwoFactor.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
type: TwoFATypes.U2F,
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let { response } = req.body;
|
||||||
|
if (
|
||||||
|
!twofactor ||
|
||||||
|
!login.data ||
|
||||||
|
login.data.type !== "ykr" ||
|
||||||
|
(special && (!special.data || special.data.type !== "ykr"))
|
||||||
|
) {
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Method!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||||
|
twofactor.valid = false;
|
||||||
|
await TwoFactor.save(twofactor);
|
||||||
|
throw new RequestError(
|
||||||
|
"Invalid Method!",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let login_exp;
|
||||||
|
let special_exp;
|
||||||
|
let result = u2f.checkSignature(
|
||||||
|
login.data.request,
|
||||||
|
response,
|
||||||
|
twofactor.data.publicKey
|
||||||
|
);
|
||||||
|
if (result.successful) {
|
||||||
|
if (special) {
|
||||||
|
let result = u2f.checkSignature(
|
||||||
|
special.data.request,
|
||||||
|
response,
|
||||||
|
twofactor.data.publicKey
|
||||||
|
);
|
||||||
|
if (result.successful) {
|
||||||
|
special_exp = await upgradeToken(special);
|
||||||
|
} else {
|
||||||
|
throw new RequestError(
|
||||||
|
result.errorMessage,
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
login_exp = await upgradeToken(login);
|
||||||
|
} else {
|
||||||
|
throw new RequestError(
|
||||||
|
result.errorMessage,
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.json({ success: true, login_exp, special_exp });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default U2FRoute;
|
75
Backend/src/config.ts
Normal file
75
Backend/src/config.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { parse } from "@hibas123/config";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
import moment = require("moment");
|
||||||
|
|
||||||
|
export const refreshTokenValidTime = moment.duration(6, "month");
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export interface DatabaseConfig {
|
||||||
|
host: string;
|
||||||
|
database: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebConfig {
|
||||||
|
port: string;
|
||||||
|
secure: "true" | "false" | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoreConfig {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
dev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
core: CoreConfig;
|
||||||
|
database: DatabaseConfig;
|
||||||
|
web: WebConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = (parse(
|
||||||
|
{
|
||||||
|
core: {
|
||||||
|
dev: {
|
||||||
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: "Open Auth",
|
||||||
|
},
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
database: {
|
||||||
|
type: String,
|
||||||
|
default: "openauth",
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
default: "localhost",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
port: {
|
||||||
|
type: Number,
|
||||||
|
default: 3004,
|
||||||
|
},
|
||||||
|
secure: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"config.ini"
|
||||||
|
) as any) as Config;
|
||||||
|
|
||||||
|
if (process.env.DEV === "true") config.core.dev = true;
|
||||||
|
if (config.core.dev)
|
||||||
|
Logging.warning(
|
||||||
|
"DEV mode active. This can cause major performance issues, data loss and vulnerabilities! "
|
||||||
|
);
|
||||||
|
|
||||||
|
export default config;
|
@ -1,19 +1,13 @@
|
|||||||
import SafeMongo from "@hibas123/safe_mongo";
|
import SafeMongo from "@hibas123/safe_mongo";
|
||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
|
let dbname = "openauth";
|
||||||
let dbname = "openauth";
|
let host = "localhost";
|
||||||
let host = "localhost";
|
if (Config.database) {
|
||||||
|
if (Config.database.database) dbname = Config.database.database;
|
||||||
if (Config.database) {
|
if (Config.database.host) host = Config.database.host;
|
||||||
if (Config.database.database) dbname = Config.database.database;
|
}
|
||||||
if (Config.database.host) host = Config.database.host;
|
if (Config.core.dev) dbname += "_dev";
|
||||||
}
|
const DB = new SafeMongo("mongodb://" + host, dbname, {
|
||||||
|
useUnifiedTopology: true,
|
||||||
if (Config.core.dev) dbname += "_dev";
|
});
|
||||||
|
export default DB;
|
||||||
const DB = new SafeMongo("mongodb://" + host, dbname, {
|
|
||||||
useUnifiedTopology: true,
|
|
||||||
useNewUrlParser: true,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
export default DB;
|
|
1
src/express.d.ts → Backend/src/express.d.ts
vendored
1
src/express.d.ts → Backend/src/express.d.ts
vendored
@ -11,7 +11,6 @@ declare module "express" {
|
|||||||
token: {
|
token: {
|
||||||
login: ILoginToken;
|
login: ILoginToken;
|
||||||
special?: ILoginToken;
|
special?: ILoginToken;
|
||||||
session?: { [key: string]: any };
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
60
Backend/src/helper/jwt.ts
Normal file
60
Backend/src/helper/jwt.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { IUser, Gender } from "../models/user";
|
||||||
|
import { ObjectID } from "bson";
|
||||||
|
import { createJWT } from "../keys";
|
||||||
|
import { IClient } from "../models/client";
|
||||||
|
import config from "../config";
|
||||||
|
import * as moment from "moment";
|
||||||
|
|
||||||
|
export interface OAuthJWT {
|
||||||
|
user: string;
|
||||||
|
username: string;
|
||||||
|
permissions: string[];
|
||||||
|
application: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuer = config.core.url;
|
||||||
|
|
||||||
|
export const IDTokenJWTExp = moment.duration(30, "m").asSeconds();
|
||||||
|
export function getIDToken(user: IUser, client_id: string, nonce: string) {
|
||||||
|
return createJWT(
|
||||||
|
{
|
||||||
|
user: user.uid,
|
||||||
|
name: user.name,
|
||||||
|
nickname: user.username,
|
||||||
|
username: user.username,
|
||||||
|
preferred_username: user.username,
|
||||||
|
gender: Gender[user.gender],
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expiresIn: IDTokenJWTExp,
|
||||||
|
issuer,
|
||||||
|
algorithm: "RS256",
|
||||||
|
subject: user.uid,
|
||||||
|
audience: client_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessTokenJWTExp = moment.duration(6, "h");
|
||||||
|
export function getAccessTokenJWT(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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expiresIn: AccessTokenJWTExp.asSeconds(),
|
||||||
|
issuer,
|
||||||
|
algorithm: "RS256",
|
||||||
|
subject: token.user.uid,
|
||||||
|
audience: token.client.client_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
7
Backend/src/helper/promiseMiddleware.ts
Normal file
7
Backend/src/helper/promiseMiddleware.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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);
|
||||||
|
};
|
@ -2,4 +2,4 @@ import { randomBytes } from "crypto";
|
|||||||
|
|
||||||
export function randomString(length: number) {
|
export function randomString(length: number) {
|
||||||
return randomBytes(length).toString("base64").slice(0, length);
|
return randomBytes(length).toString("base64").slice(0, length);
|
||||||
}
|
}
|
@ -1,388 +1,390 @@
|
|||||||
|
/**
|
||||||
/**
|
* Hypertext Transfer Protocol (HTTP) response status codes.
|
||||||
* Hypertext Transfer Protocol (HTTP) response status codes.
|
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
|
||||||
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
|
*/
|
||||||
*/
|
export enum HttpStatusCode {
|
||||||
export enum HttpStatusCode {
|
/**
|
||||||
|
* The server has received the request headers and the client should proceed to send the request body
|
||||||
/**
|
* (in the case of a request for which a body needs to be sent; for example, a POST request).
|
||||||
* The server has received the request headers and the client should proceed to send the request body
|
* Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient.
|
||||||
* (in the case of a request for which a body needs to be sent; for example, a POST request).
|
* To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request
|
||||||
* Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient.
|
* 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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
CONTINUE = 100,
|
/**
|
||||||
|
* The requester has asked the server to switch protocols and the server has agreed to do so.
|
||||||
/**
|
*/
|
||||||
* The requester has asked the server to switch protocols and the server has agreed to do so.
|
SWITCHING_PROTOCOLS = 101,
|
||||||
*/
|
|
||||||
SWITCHING_PROTOCOLS = 101,
|
/**
|
||||||
|
* A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request.
|
||||||
/**
|
* This code indicates that the server has received and is processing the request, but no response is available yet.
|
||||||
* A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request.
|
* This prevents the client from timing out and assuming the request was lost.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
PROCESSING = 102,
|
/**
|
||||||
|
* Standard response for successful HTTP requests.
|
||||||
/**
|
* The actual response will depend on the request method used.
|
||||||
* Standard response for successful HTTP requests.
|
* In a GET request, the response will contain an entity corresponding to the requested resource.
|
||||||
* The actual response will depend on the request method used.
|
* In a POST request, the response will contain an entity describing or containing the result of the action.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
OK = 200,
|
/**
|
||||||
|
* The request has been fulfilled, resulting in the creation of a new resource.
|
||||||
/**
|
*/
|
||||||
* The request has been fulfilled, resulting in the creation of a new resource.
|
CREATED = 201,
|
||||||
*/
|
|
||||||
CREATED = 201,
|
/**
|
||||||
|
* The request has been accepted for processing, but the processing has not been completed.
|
||||||
/**
|
* The request might or might not be eventually acted upon, and may be disallowed when processing occurs.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
ACCEPTED = 202,
|
/**
|
||||||
|
* SINCE HTTP/1.1
|
||||||
/**
|
* The server is a transforming proxy that received a 200 OK from its origin,
|
||||||
* SINCE HTTP/1.1
|
* but is returning a modified version of the origin's response.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
NON_AUTHORITATIVE_INFORMATION = 203,
|
/**
|
||||||
|
* The server successfully processed the request and is not returning any content.
|
||||||
/**
|
*/
|
||||||
* The server successfully processed the request and is not returning any content.
|
NO_CONTENT = 204,
|
||||||
*/
|
|
||||||
NO_CONTENT = 204,
|
/**
|
||||||
|
* The server successfully processed the request, but is not returning any content.
|
||||||
/**
|
* Unlike a 204 response, this response requires that the requester reset the document view.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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,
|
||||||
* The server is delivering only part of the resource (byte serving) due to a range header sent by the client.
|
* or split a download into multiple simultaneous streams.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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,
|
||||||
* Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation).
|
* to list files with different filename extensions, or to suggest word-sense disambiguation.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
MULTIPLE_CHOICES = 300,
|
/**
|
||||||
|
* This and all future requests should be directed to the given URI.
|
||||||
/**
|
*/
|
||||||
* This and all future requests should be directed to the given URI.
|
MOVED_PERMANENTLY = 301,
|
||||||
*/
|
|
||||||
MOVED_PERMANENTLY = 301,
|
/**
|
||||||
|
* This is an example of industry practice contradicting the standard.
|
||||||
/**
|
* The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect
|
||||||
* This is an example of industry practice contradicting the standard.
|
* (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302
|
||||||
* The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect
|
* with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307
|
||||||
* (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302
|
* to distinguish between the two behaviours. However, some Web applications and frameworks
|
||||||
* with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307
|
* use the 302 status code as if it were the 303.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
FOUND = 302,
|
/**
|
||||||
|
* SINCE HTTP/1.1
|
||||||
/**
|
* The response to the request can be found under another URI using a GET method.
|
||||||
* SINCE HTTP/1.1
|
* When received in response to a POST (or PUT/DELETE), the client should presume that
|
||||||
* The response to the request can be found under another URI using a GET method.
|
* the server has received the data and should issue a redirect with a separate GET message.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* SINCE HTTP/1.1
|
* Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
USE_PROXY = 305,
|
/**
|
||||||
|
* No longer used. Originally meant "Subsequent requests should use the specified proxy."
|
||||||
/**
|
*/
|
||||||
* No longer used. Originally meant "Subsequent requests should use the specified proxy."
|
SWITCH_PROXY = 306,
|
||||||
*/
|
|
||||||
SWITCH_PROXY = 306,
|
/**
|
||||||
|
* SINCE HTTP/1.1
|
||||||
/**
|
* In this case, the request should be repeated with another URI; however, future requests should still use the original URI.
|
||||||
* SINCE HTTP/1.1
|
* In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request.
|
||||||
* In this case, the request should be repeated with another URI; however, future requests should still use the original URI.
|
* For example, a POST request should be repeated using another POST request.
|
||||||
* In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request.
|
*/
|
||||||
* For example, a POST request should be repeated using another POST request.
|
TEMPORARY_REDIRECT = 307,
|
||||||
*/
|
|
||||||
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.
|
||||||
* The request and all future requests should be repeated using another URI.
|
* So, for example, submitting a form to a permanently redirected resource may continue smoothly.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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).
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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
|
||||||
* Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet
|
* requested resource. See Basic access authentication and Digest access authentication. 401 semantically means
|
||||||
* been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the
|
* "unauthenticated",i.e. the user does not have the necessary credentials.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* Reserved for future use. The original intention was that this code might be used as part of some form of digital
|
* Google Developers API uses this status if a particular developer has exceeded the daily limit on requests.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
PAYMENT_REQUIRED = 402,
|
/**
|
||||||
|
* The request was valid, but the server is refusing action.
|
||||||
/**
|
* The user might not have the necessary permissions for a resource.
|
||||||
* The request was valid, but the server is refusing action.
|
*/
|
||||||
* The user might not have the necessary permissions for a resource.
|
FORBIDDEN = 403,
|
||||||
*/
|
|
||||||
FORBIDDEN = 403,
|
/**
|
||||||
|
* The requested resource could not be found but may be available in the future.
|
||||||
/**
|
* Subsequent requests by the client are permissible.
|
||||||
* The requested resource could not be found but may be available in the future.
|
*/
|
||||||
* Subsequent requests by the client are permissible.
|
NOT_FOUND = 404,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
METHOD_NOT_ALLOWED = 405,
|
/**
|
||||||
|
* The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request.
|
||||||
/**
|
*/
|
||||||
* The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request.
|
NOT_ACCEPTABLE = 406,
|
||||||
*/
|
|
||||||
NOT_ACCEPTABLE = 406,
|
/**
|
||||||
|
* The client must first authenticate itself with the proxy.
|
||||||
/**
|
*/
|
||||||
* The client must first authenticate itself with the proxy.
|
PROXY_AUTHENTICATION_REQUIRED = 407,
|
||||||
*/
|
|
||||||
PROXY_AUTHENTICATION_REQUIRED = 407,
|
/**
|
||||||
|
* The server timed out waiting for the request.
|
||||||
/**
|
* According to HTTP specifications:
|
||||||
* The server timed out waiting for the request.
|
* "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."
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* Indicates that the resource requested is no longer available and will not be available again.
|
* Upon receiving a 410 status code, the client should not request the resource in the future.
|
||||||
* This should be used when a resource has been intentionally removed and the resource should be purged.
|
* Clients such as search engines should remove the resource from their indices.
|
||||||
* Upon receiving a 410 status code, the client should not request the resource in the future.
|
* Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
GONE = 410,
|
/**
|
||||||
|
* The request did not specify the length of its content, which is required by the requested resource.
|
||||||
/**
|
*/
|
||||||
* The request did not specify the length of its content, which is required by the requested resource.
|
LENGTH_REQUIRED = 411,
|
||||||
*/
|
|
||||||
LENGTH_REQUIRED = 411,
|
/**
|
||||||
|
* The server does not meet one of the preconditions that the requester put on the request.
|
||||||
/**
|
*/
|
||||||
* The server does not meet one of the preconditions that the requester put on the request.
|
PRECONDITION_FAILED = 412,
|
||||||
*/
|
|
||||||
PRECONDITION_FAILED = 412,
|
/**
|
||||||
|
* The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large".
|
||||||
/**
|
*/
|
||||||
* The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large".
|
PAYLOAD_TOO_LARGE = 413,
|
||||||
*/
|
|
||||||
PAYLOAD_TOO_LARGE = 413,
|
/**
|
||||||
|
* The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request,
|
||||||
/**
|
* in which case it should be converted to a POST request.
|
||||||
* The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request,
|
* Called "Request-URI Too Long" previously.
|
||||||
* in which case it should be converted to a POST request.
|
*/
|
||||||
* Called "Request-URI Too Long" previously.
|
URI_TOO_LONG = 414,
|
||||||
*/
|
|
||||||
URI_TOO_LONG = 414,
|
/**
|
||||||
|
* The request entity has a media type which the server or resource does not support.
|
||||||
/**
|
* For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* The client has asked for a portion of the file (byte serving), but the server cannot supply that portion.
|
* Called "Requested Range Not Satisfiable" previously.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
RANGE_NOT_SATISFIABLE = 416,
|
/**
|
||||||
|
* The server cannot meet the requirements of the Expect request-header field.
|
||||||
/**
|
*/
|
||||||
* The server cannot meet the requirements of the Expect request-header field.
|
EXPECTATION_FAILED = 417,
|
||||||
*/
|
|
||||||
EXPECTATION_FAILED = 417,
|
/**
|
||||||
|
* This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol,
|
||||||
/**
|
* and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by
|
||||||
* This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol,
|
* teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
I_AM_A_TEAPOT = 418,
|
/**
|
||||||
|
* The request was directed at a server that is not able to produce a response (for example because a connection reuse).
|
||||||
/**
|
*/
|
||||||
* The request was directed at a server that is not able to produce a response (for example because a connection reuse).
|
MISDIRECTED_REQUEST = 421,
|
||||||
*/
|
|
||||||
MISDIRECTED_REQUEST = 421,
|
/**
|
||||||
|
* The request was well-formed but was unable to be followed due to semantic errors.
|
||||||
/**
|
*/
|
||||||
* The request was well-formed but was unable to be followed due to semantic errors.
|
UNPROCESSABLE_ENTITY = 422,
|
||||||
*/
|
|
||||||
UNPROCESSABLE_ENTITY = 422,
|
/**
|
||||||
|
* The resource that is being accessed is locked.
|
||||||
/**
|
*/
|
||||||
* The resource that is being accessed is locked.
|
LOCKED = 423,
|
||||||
*/
|
|
||||||
LOCKED = 423,
|
/**
|
||||||
|
* The request failed due to failure of a previous request (e.g., a PROPPATCH).
|
||||||
/**
|
*/
|
||||||
* The request failed due to failure of a previous request (e.g., a PROPPATCH).
|
FAILED_DEPENDENCY = 424,
|
||||||
*/
|
|
||||||
FAILED_DEPENDENCY = 424,
|
/**
|
||||||
|
* The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field.
|
||||||
/**
|
*/
|
||||||
* The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field.
|
UPGRADE_REQUIRED = 426,
|
||||||
*/
|
|
||||||
UPGRADE_REQUIRED = 426,
|
/**
|
||||||
|
* The origin server requires the request to be conditional.
|
||||||
/**
|
* Intended to prevent "the 'lost update' problem, where a client
|
||||||
* The origin server requires the request to be conditional.
|
* GETs a resource's state, modifies it, and PUTs it back to the server,
|
||||||
* Intended to prevent "the 'lost update' problem, where a client
|
* when meanwhile a third party has modified the state on the server, leading to a conflict."
|
||||||
* 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,
|
||||||
*/
|
|
||||||
PRECONDITION_REQUIRED = 428,
|
/**
|
||||||
|
* The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes.
|
||||||
/**
|
*/
|
||||||
* The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes.
|
TOO_MANY_REQUESTS = 429,
|
||||||
*/
|
|
||||||
TOO_MANY_REQUESTS = 429,
|
/**
|
||||||
|
* The server is unwilling to process the request because either an individual header field,
|
||||||
/**
|
* or all the header fields collectively, are too large.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
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.
|
||||||
* 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,
|
||||||
*/
|
|
||||||
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
|
/**
|
||||||
|
* A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.
|
||||||
/**
|
*/
|
||||||
* A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.
|
INTERNAL_SERVER_ERROR = 500,
|
||||||
*/
|
|
||||||
INTERNAL_SERVER_ERROR = 500,
|
/**
|
||||||
|
* The server either does not recognize the request method, or it lacks the ability to fulfill the request.
|
||||||
/**
|
* Usually this implies future availability (e.g., a new feature of a web-service API).
|
||||||
* 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,
|
||||||
*/
|
|
||||||
NOT_IMPLEMENTED = 501,
|
/**
|
||||||
|
* The server was acting as a gateway or proxy and received an invalid response from the upstream server.
|
||||||
/**
|
*/
|
||||||
* The server was acting as a gateway or proxy and received an invalid response from the upstream server.
|
BAD_GATEWAY = 502,
|
||||||
*/
|
|
||||||
BAD_GATEWAY = 502,
|
/**
|
||||||
|
* The server is currently unavailable (because it is overloaded or down for maintenance).
|
||||||
/**
|
* Generally, this is a temporary state.
|
||||||
* The server is currently unavailable (because it is overloaded or down for maintenance).
|
*/
|
||||||
* Generally, this is a temporary state.
|
SERVICE_UNAVAILABLE = 503,
|
||||||
*/
|
|
||||||
SERVICE_UNAVAILABLE = 503,
|
/**
|
||||||
|
* The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.
|
||||||
/**
|
*/
|
||||||
* The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.
|
GATEWAY_TIMEOUT = 504,
|
||||||
*/
|
|
||||||
GATEWAY_TIMEOUT = 504,
|
/**
|
||||||
|
* The server does not support the HTTP protocol version used in the request
|
||||||
/**
|
*/
|
||||||
* The server does not support the HTTP protocol version used in the request
|
HTTP_VERSION_NOT_SUPPORTED = 505,
|
||||||
*/
|
|
||||||
HTTP_VERSION_NOT_SUPPORTED = 505,
|
/**
|
||||||
|
* Transparent content negotiation for the request results in a circular reference.
|
||||||
/**
|
*/
|
||||||
* Transparent content negotiation for the request results in a circular reference.
|
VARIANT_ALSO_NEGOTIATES = 506,
|
||||||
*/
|
|
||||||
VARIANT_ALSO_NEGOTIATES = 506,
|
/**
|
||||||
|
* The server is unable to store the representation needed to complete the request.
|
||||||
/**
|
*/
|
||||||
* The server is unable to store the representation needed to complete the request.
|
INSUFFICIENT_STORAGE = 507,
|
||||||
*/
|
|
||||||
INSUFFICIENT_STORAGE = 507,
|
/**
|
||||||
|
* The server detected an infinite loop while processing the request.
|
||||||
/**
|
*/
|
||||||
* The server detected an infinite loop while processing the request.
|
LOOP_DETECTED = 508,
|
||||||
*/
|
|
||||||
LOOP_DETECTED = 508,
|
/**
|
||||||
|
* Further extensions to the request are required for the server to fulfill it.
|
||||||
/**
|
*/
|
||||||
* Further extensions to the request are required for the server to fulfill it.
|
NOT_EXTENDED = 510,
|
||||||
*/
|
|
||||||
NOT_EXTENDED = 510,
|
/**
|
||||||
|
* The client needs to authenticate to gain network access.
|
||||||
/**
|
* Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used
|
||||||
* The client needs to authenticate to gain network access.
|
* to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot).
|
||||||
* 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,
|
||||||
*/
|
}
|
||||||
NETWORK_AUTHENTICATION_REQUIRED = 511
|
|
||||||
}
|
export default class RequestError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: any,
|
||||||
export default class RequestError extends Error {
|
public status: HttpStatusCode,
|
||||||
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false, public additional: any = undefined) {
|
public nolog: boolean = false,
|
||||||
super("")
|
public additional: any = undefined
|
||||||
this.message = message;
|
) {
|
||||||
}
|
super("");
|
||||||
}
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
18
Backend/src/helper/user_key.ts
Normal file
18
Backend/src/helper/user_key.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// import * as crypto from "crypto-js"
|
||||||
|
import { IUser } from "../models/user";
|
||||||
|
import { IClient } from "../models/client";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
|
function sha512(text: string) {
|
||||||
|
let hash = crypto.createHash("sha512");
|
||||||
|
hash.update(text);
|
||||||
|
return hash.digest("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEncryptionKey(user: IUser, client: IClient) {
|
||||||
|
return sha512(
|
||||||
|
sha512(user.encryption_key) +
|
||||||
|
sha512(client._id.toHexString()) +
|
||||||
|
sha512(client.client_id)
|
||||||
|
);
|
||||||
|
}
|
90
Backend/src/index.ts
Normal file
90
Backend/src/index.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
import config from "./config";
|
||||||
|
|
||||||
|
// import NLS from "@hibas123/nodeloggingserver_client";
|
||||||
|
// if (config.logging) {
|
||||||
|
// let s = NLS(Logging, config.logging.server, config.logging.appid, config.logging.token);
|
||||||
|
// s.send(`[${new Date().toLocaleTimeString()}] Starting application`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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.core.dev) {
|
||||||
|
Logging.warning("Running in dev mode! Database will be cleared!");
|
||||||
|
}
|
||||||
|
DB.connect()
|
||||||
|
.then(async () => {
|
||||||
|
Logging.log("Database connected", config);
|
||||||
|
if (config.core.dev) await TestData();
|
||||||
|
let web = new Web(config.web);
|
||||||
|
web.listen();
|
||||||
|
|
||||||
|
let already = new Set();
|
||||||
|
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);
|
||||||
|
let msg = `${me} /${path
|
||||||
|
.concat(split(layer.regexp))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("/")}`;
|
||||||
|
if (!already.has(msg)) {
|
||||||
|
already.add(msg);
|
||||||
|
Logging.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
@ -1,10 +1,10 @@
|
|||||||
import Logging from "@hibas123/nodelogging";
|
import Logging from "@hibas123/nodelogging";
|
||||||
import * as fs from "fs"
|
import * as fs from "fs";
|
||||||
|
|
||||||
let private_key: string;
|
let private_key: string;
|
||||||
let rsa: RSA;
|
let rsa: RSA;
|
||||||
export function sign(message: Buffer): Buffer {
|
export function sign(message: Buffer): Buffer {
|
||||||
return rsa.sign(message, "buffer")
|
return rsa.sign(message, "buffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verify(message: Buffer, signature: Buffer): boolean {
|
export function verify(message: Buffer, signature: Buffer): boolean {
|
||||||
@ -19,28 +19,28 @@ import config from "./config";
|
|||||||
export function createJWT(payload: any, options: jwt.SignOptions) {
|
export function createJWT(payload: any, options: jwt.SignOptions) {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
return jwt.sign(payload, private_key, options, (err, token) => {
|
return jwt.sign(payload, private_key, options, (err, token) => {
|
||||||
if (err) reject(err)
|
if (err) reject(err);
|
||||||
else resolve(token)
|
else resolve(token);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateJWT(data: string) {
|
export async function validateJWT(data: string) {
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
jwt.verify(data, public_key, (err, valid) => {
|
jwt.verify(data, public_key, (err, valid) => {
|
||||||
if (err) reject(err)
|
if (err) reject(err);
|
||||||
else resolve(valid)
|
else resolve(valid);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let create = false;
|
let create = false;
|
||||||
if (fs.existsSync("./keys")) {
|
if (fs.existsSync("./keys")) {
|
||||||
if (fs.existsSync("./keys/private.pem")) {
|
if (fs.existsSync("./keys/private.pem")) {
|
||||||
if (fs.existsSync("./keys/public.pem")) {
|
if (fs.existsSync("./keys/public.pem")) {
|
||||||
Logging.log("Using existing private and public key")
|
Logging.log("Using existing private and public key");
|
||||||
private_key = fs.readFileSync("./keys/private.pem").toString("utf8")
|
private_key = fs.readFileSync("./keys/private.pem").toString("utf8");
|
||||||
public_key = fs.readFileSync("./keys/public.pem").toString("utf8")
|
public_key = fs.readFileSync("./keys/public.pem").toString("utf8");
|
||||||
|
|
||||||
if (!private_key || !public_key) {
|
if (!private_key || !public_key) {
|
||||||
create = true;
|
create = true;
|
||||||
@ -49,21 +49,21 @@ if (fs.existsSync("./keys")) {
|
|||||||
} else create = true;
|
} else create = true;
|
||||||
} else create = true;
|
} else create = true;
|
||||||
|
|
||||||
import * as RSA from "node-rsa"
|
import * as RSA from "node-rsa";
|
||||||
|
|
||||||
if (create === true) {
|
if (create === true) {
|
||||||
Logging.log("Started RSA Key gen")
|
Logging.log("Started RSA Key gen");
|
||||||
let rsa = new RSA({ b: 4096 });
|
let rsa = new RSA({ b: 4096 });
|
||||||
private_key = rsa.exportKey("private")
|
private_key = rsa.exportKey("private");
|
||||||
public_key = rsa.exportKey("public")
|
public_key = rsa.exportKey("public");
|
||||||
|
|
||||||
if (!fs.existsSync("./keys")) {
|
if (!fs.existsSync("./keys")) {
|
||||||
fs.mkdirSync("./keys")
|
fs.mkdirSync("./keys");
|
||||||
}
|
}
|
||||||
fs.writeFileSync("./keys/private.pem", private_key)
|
fs.writeFileSync("./keys/private.pem", private_key);
|
||||||
fs.writeFileSync("./keys/public.pem", public_key)
|
fs.writeFileSync("./keys/public.pem", public_key);
|
||||||
Logging.log("Key pair generated")
|
Logging.log("Key pair generated");
|
||||||
}
|
}
|
||||||
|
|
||||||
rsa = new RSA(private_key, "private")
|
rsa = new RSA(private_key, "private");
|
||||||
rsa.importKey(public_key, "public")
|
rsa.importKey(public_key, "public");
|
@ -4,21 +4,23 @@ import { ObjectID } from "mongodb";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export interface IClient extends ModelDataBase {
|
export interface IClient extends ModelDataBase {
|
||||||
maintainer: ObjectID
|
maintainer: ObjectID;
|
||||||
internal: boolean
|
internal: boolean;
|
||||||
name: string
|
name: string;
|
||||||
redirect_url: string
|
redirect_url: string;
|
||||||
website: string
|
website: string;
|
||||||
logo?: string
|
logo?: string;
|
||||||
client_id: string
|
client_id: string;
|
||||||
client_secret: string
|
client_secret: string;
|
||||||
|
featured?: boolean;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Client = DB.addModel<IClient>({
|
const Client = DB.addModel<IClient>({
|
||||||
name: "client",
|
name: "client",
|
||||||
versions: [
|
versions: [
|
||||||
{
|
{
|
||||||
migration: () => { },
|
migration: () => {},
|
||||||
schema: {
|
schema: {
|
||||||
maintainer: { type: ObjectID },
|
maintainer: { type: ObjectID },
|
||||||
internal: { type: Boolean, default: false },
|
internal: { type: Boolean, default: false },
|
||||||
@ -27,10 +29,12 @@ const Client = DB.addModel<IClient>({
|
|||||||
website: { type: String },
|
website: { type: String },
|
||||||
logo: { type: String, optional: true },
|
logo: { type: String, optional: true },
|
||||||
client_id: { type: String, default: () => v4() },
|
client_id: { type: String, default: () => v4() },
|
||||||
client_secret: { type: String }
|
client_secret: { type: String },
|
||||||
}
|
featured: { type: Boolean, optional: true },
|
||||||
}
|
description: { type: String, optional: true },
|
||||||
]
|
},
|
||||||
})
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export default Client;
|
export default Client;
|
29
Backend/src/models/client_code.ts
Normal file
29
Backend/src/models/client_code.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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;
|
@ -10,14 +10,16 @@ export interface IGrant extends ModelDataBase {
|
|||||||
|
|
||||||
const Grant = DB.addModel<IGrant>({
|
const Grant = DB.addModel<IGrant>({
|
||||||
name: "grant",
|
name: "grant",
|
||||||
versions: [{
|
versions: [
|
||||||
migration: () => { },
|
{
|
||||||
schema: {
|
migration: () => {},
|
||||||
user: { type: ObjectID },
|
schema: {
|
||||||
client: { type: ObjectID },
|
user: { type: ObjectID },
|
||||||
permissions: { type: ObjectID, array: true }
|
client: { type: ObjectID },
|
||||||
}
|
permissions: { type: ObjectID, array: true },
|
||||||
}]
|
},
|
||||||
})
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export default Grant;
|
export default Grant;
|
76
Backend/src/models/login_token.ts
Normal file
76
Backend/src/models/login_token.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import DB from "../database";
|
||||||
|
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||||
|
import { ObjectID } from "mongodb";
|
||||||
|
import moment = require("moment");
|
||||||
|
|
||||||
|
export interface ILoginToken extends ModelDataBase {
|
||||||
|
token: string;
|
||||||
|
special: boolean;
|
||||||
|
user: ObjectID;
|
||||||
|
validTill: Date;
|
||||||
|
valid: boolean;
|
||||||
|
validated: boolean;
|
||||||
|
data: any;
|
||||||
|
ip: string;
|
||||||
|
browser: string;
|
||||||
|
}
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
migration: (doc: ILoginToken) => {
|
||||||
|
doc.validated = true;
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
token: { type: String },
|
||||||
|
special: { type: Boolean, default: () => false },
|
||||||
|
user: { type: ObjectID },
|
||||||
|
validTill: { type: Date },
|
||||||
|
valid: { type: Boolean },
|
||||||
|
validated: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
migration: (doc: ILoginToken) => {
|
||||||
|
doc.validated = true;
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
token: { type: String },
|
||||||
|
special: { type: Boolean, default: () => false },
|
||||||
|
user: { type: ObjectID },
|
||||||
|
validTill: { type: Date },
|
||||||
|
valid: { type: Boolean },
|
||||||
|
validated: { type: Boolean, default: false },
|
||||||
|
data: { type: "any", optional: true },
|
||||||
|
ip: { type: String, optional: true },
|
||||||
|
browser: { type: String, optional: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function CheckToken(
|
||||||
|
token: ILoginToken,
|
||||||
|
validated: boolean = true
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!token || !token.valid) return false;
|
||||||
|
if (validated && !token.validated) return false;
|
||||||
|
if (moment().isAfter(token.validTill)) {
|
||||||
|
token.valid = false;
|
||||||
|
await LoginToken.save(token);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginToken;
|
24
Backend/src/models/mail.ts
Normal file
24
Backend/src/models/mail.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import DB from "../database";
|
||||||
|
import { ModelDataBase } from "@hibas123/safe_mongo";
|
||||||
|
|
||||||
|
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;
|
37
Backend/src/models/permissions.ts
Normal file
37
Backend/src/models/permissions.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import DB from "../database";
|
||||||
|
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||||
|
import { ObjectID } from "mongodb";
|
||||||
|
|
||||||
|
export interface IPermission extends ModelDataBase {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
client: ObjectID;
|
||||||
|
grant_type: "user" | "client";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Permission = DB.addModel<IPermission>({
|
||||||
|
name: "permission",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
migration: () => {},
|
||||||
|
schema: {
|
||||||
|
name: { type: String },
|
||||||
|
description: { type: String },
|
||||||
|
client: { type: ObjectID },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
migration: (old) => {
|
||||||
|
old.grant_type = "user";
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
name: { type: String },
|
||||||
|
description: { type: String },
|
||||||
|
client: { type: ObjectID },
|
||||||
|
grant_type: { type: String, default: "user" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Permission;
|
@ -14,17 +14,19 @@ export interface IRefreshToken extends ModelDataBase {
|
|||||||
|
|
||||||
const RefreshToken = DB.addModel<IRefreshToken>({
|
const RefreshToken = DB.addModel<IRefreshToken>({
|
||||||
name: "refresh_token",
|
name: "refresh_token",
|
||||||
versions: [{
|
versions: [
|
||||||
migration: () => { },
|
{
|
||||||
schema: {
|
migration: () => {},
|
||||||
token: { type: String },
|
schema: {
|
||||||
user: { type: ObjectID },
|
token: { type: String },
|
||||||
client: { type: ObjectID },
|
user: { type: ObjectID },
|
||||||
permissions: { type: Array },
|
client: { type: ObjectID },
|
||||||
validTill: { type: Date },
|
permissions: { type: Array },
|
||||||
valid: { type: Boolean }
|
validTill: { type: Date },
|
||||||
}
|
valid: { type: Boolean },
|
||||||
}]
|
},
|
||||||
})
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export default RefreshToken;
|
export default RefreshToken;
|
@ -11,15 +11,17 @@ export interface IRegCode extends ModelDataBase {
|
|||||||
|
|
||||||
const RegCode = DB.addModel<IRegCode>({
|
const RegCode = DB.addModel<IRegCode>({
|
||||||
name: "reg_code",
|
name: "reg_code",
|
||||||
versions: [{
|
versions: [
|
||||||
migration: () => { },
|
{
|
||||||
schema: {
|
migration: () => {},
|
||||||
token: { type: String },
|
schema: {
|
||||||
valid: { type: Boolean },
|
token: { type: String },
|
||||||
validTill: { type: Date }
|
valid: { type: Boolean },
|
||||||
}
|
validTill: { type: Date },
|
||||||
}]
|
},
|
||||||
})
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export default RegCode;
|
export default RegCode;
|
||||||
|
|
||||||
@ -52,4 +54,4 @@ export default RegCode;
|
|||||||
// @Column
|
// @Column
|
||||||
// @DeletedAt
|
// @DeletedAt
|
||||||
// deletionDate: Date;
|
// deletionDate: Date;
|
||||||
// }
|
// }
|
@ -1,67 +1,69 @@
|
|||||||
import DB from "../database";
|
import DB from "../database";
|
||||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||||
import { ObjectID } from "bson";
|
import { ObjectID } from "bson";
|
||||||
|
|
||||||
export enum TFATypes {
|
export enum TFATypes {
|
||||||
OTC,
|
OTC,
|
||||||
BACKUP_CODE,
|
BACKUP_CODE,
|
||||||
U2F,
|
U2F,
|
||||||
APP_ALLOW
|
APP_ALLOW,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TFANames = new Map<TFATypes, string>();
|
export const TFANames = new Map<TFATypes, string>();
|
||||||
TFANames.set(TFATypes.OTC, "Authenticator");
|
TFANames.set(TFATypes.OTC, "Authenticator");
|
||||||
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
|
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
|
||||||
TFANames.set(TFATypes.U2F, "Security Key (U2F)");
|
TFANames.set(TFATypes.U2F, "Security Key (U2F)");
|
||||||
TFANames.set(TFATypes.APP_ALLOW, "App Push");
|
TFANames.set(TFATypes.APP_ALLOW, "App Push");
|
||||||
|
|
||||||
export interface ITwoFactor extends ModelDataBase {
|
export interface ITwoFactor extends ModelDataBase {
|
||||||
user: ObjectID
|
user: ObjectID;
|
||||||
valid: boolean
|
valid: boolean;
|
||||||
expires?: Date;
|
expires?: Date;
|
||||||
name?: string;
|
name?: string;
|
||||||
type: TFATypes
|
type: TFATypes;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOTC extends ITwoFactor {
|
export interface IOTC extends ITwoFactor {
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IYubiKey extends ITwoFactor {
|
export interface IYubiKey extends ITwoFactor {
|
||||||
data: {
|
data: {
|
||||||
registration?: any;
|
registration?: any;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
keyHandle: string;
|
keyHandle: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IU2F extends ITwoFactor {
|
export interface IU2F extends ITwoFactor {
|
||||||
data: {
|
data: {
|
||||||
challenge?: string;
|
challenge?: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
keyHandle: string;
|
keyHandle: string;
|
||||||
registration?: string;
|
registration?: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBackupCode extends ITwoFactor {
|
export interface IBackupCode extends ITwoFactor {
|
||||||
data: string[];
|
data: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TwoFactor = DB.addModel<ITwoFactor>({
|
const TwoFactor = DB.addModel<ITwoFactor>({
|
||||||
name: "twofactor",
|
name: "twofactor",
|
||||||
versions: [{
|
versions: [
|
||||||
migration: (e) => { },
|
{
|
||||||
schema: {
|
migration: (e) => {},
|
||||||
user: { type: ObjectID },
|
schema: {
|
||||||
valid: { type: Boolean },
|
user: { type: ObjectID },
|
||||||
expires: { type: Date, optional: true },
|
valid: { type: Boolean },
|
||||||
name: { type: String, optional: true },
|
expires: { type: Date, optional: true },
|
||||||
type: { type: Number },
|
name: { type: String, optional: true },
|
||||||
data: { type: "any" },
|
type: { type: Number },
|
||||||
}
|
data: { type: "any" },
|
||||||
}]
|
},
|
||||||
});
|
},
|
||||||
|
],
|
||||||
export default TwoFactor;
|
});
|
||||||
|
|
||||||
|
export default TwoFactor;
|
134
Backend/src/models/user.ts
Normal file
134
Backend/src/models/user.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import DB from "../database";
|
||||||
|
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||||
|
import { ObjectID } from "mongodb";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { randomString } from "../helper/random";
|
||||||
|
|
||||||
|
export enum Gender {
|
||||||
|
none,
|
||||||
|
male,
|
||||||
|
female,
|
||||||
|
other,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }[];
|
||||||
|
encryption_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
migration: (e: IUser) => {
|
||||||
|
e.encryption_key = randomString(64);
|
||||||
|
},
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encryption_key: {
|
||||||
|
type: String,
|
||||||
|
default: () => randomString(64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
migration: (e: any) => {
|
||||||
|
delete e.twofactor;
|
||||||
|
},
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encryption_key: {
|
||||||
|
type: String,
|
||||||
|
default: () => randomString(64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default User;
|
@ -1,144 +1,146 @@
|
|||||||
import User, { Gender } from "./models/user";
|
import User, { Gender } from "./models/user";
|
||||||
import Client from "./models/client";
|
import Client from "./models/client";
|
||||||
import { Logging } from "@hibas123/nodelogging";
|
import Logging from "@hibas123/nodelogging";
|
||||||
import RegCode from "./models/regcodes";
|
import RegCode from "./models/regcodes";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import Permission from "./models/permissions";
|
import Permission from "./models/permissions";
|
||||||
import { ObjectID } from "bson";
|
import { ObjectID } from "bson";
|
||||||
import DB from "./database";
|
import DB from "./database";
|
||||||
import TwoFactor from "./models/twofactor";
|
import TwoFactor from "./models/twofactor";
|
||||||
|
|
||||||
|
import * as speakeasy from "speakeasy";
|
||||||
import * as speakeasy from "speakeasy";
|
import LoginToken from "./models/login_token";
|
||||||
import LoginToken from "./models/login_token";
|
import Mail from "./models/mail";
|
||||||
import Mail from "./models/mail";
|
|
||||||
|
export default async function TestData() {
|
||||||
export default async function TestData() {
|
Logging.warn("Running in dev mode! Database will be cleared!");
|
||||||
await DB.db.dropDatabase();
|
await DB.db.dropDatabase();
|
||||||
|
|
||||||
let mail = await Mail.findOne({ mail: "test@test.de" });
|
let mail = await Mail.findOne({ mail: "test@test.de" });
|
||||||
if (!mail) {
|
if (!mail) {
|
||||||
mail = Mail.new({
|
mail = Mail.new({
|
||||||
mail: "test@test.de",
|
mail: "test@test.de",
|
||||||
primary: true,
|
primary: true,
|
||||||
verified: true
|
verified: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
await Mail.save(mail);
|
await Mail.save(mail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let u = await User.findOne({ username: "test" });
|
||||||
let u = await User.findOne({ username: "test" });
|
if (!u) {
|
||||||
if (!u) {
|
Logging.log("Adding test user");
|
||||||
Logging.log("Adding test user");
|
u = User.new({
|
||||||
u = User.new({
|
username: "test",
|
||||||
username: "test",
|
birthday: new Date(),
|
||||||
birthday: new Date(),
|
gender: Gender.male,
|
||||||
gender: Gender.male,
|
name: "Test Test",
|
||||||
name: "Test Test",
|
password:
|
||||||
password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
|
"125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
|
||||||
salt: "test",
|
salt: "test",
|
||||||
admin: true,
|
admin: true,
|
||||||
phones: [
|
phones: [
|
||||||
{ phone: "+4915962855955", primary: true, verified: true },
|
{ phone: "+4915962855955", primary: true, verified: true },
|
||||||
{ phone: "+4915962855932", primary: false, verified: false }
|
{ phone: "+4915962855932", primary: false, verified: false },
|
||||||
],
|
],
|
||||||
mails: [mail._id]
|
mails: [mail._id],
|
||||||
})
|
});
|
||||||
await User.save(u);
|
await User.save(u);
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = await Client.findOne({ client_id: "test001" });
|
let c = await Client.findOne({ client_id: "test001" });
|
||||||
if (!c) {
|
if (!c) {
|
||||||
Logging.log("Adding test client")
|
Logging.log("Adding test client");
|
||||||
c = Client.new({
|
c = Client.new({
|
||||||
client_id: "test001",
|
client_id: "test001",
|
||||||
client_secret: "test001",
|
client_secret: "test001",
|
||||||
internal: true,
|
internal: true,
|
||||||
maintainer: u._id,
|
maintainer: u._id,
|
||||||
name: "Test Client",
|
name: "Test Client",
|
||||||
website: "http://example.com",
|
website: "http://example.com",
|
||||||
redirect_url: "http://example.com"
|
redirect_url: "http://example.com",
|
||||||
})
|
featured: true,
|
||||||
await Client.save(c);
|
description:
|
||||||
}
|
"This client is just for testing purposes. It does not have any functionality.",
|
||||||
|
});
|
||||||
let perm = await Permission.findOne({ id: 0 });
|
await Client.save(c);
|
||||||
if (!perm) {
|
}
|
||||||
Logging.log("Adding test permission")
|
|
||||||
perm = Permission.new({
|
let perm = await Permission.findById("507f1f77bcf86cd799439011");
|
||||||
_id: new ObjectID("507f1f77bcf86cd799439011"),
|
if (!perm) {
|
||||||
name: "TestPerm",
|
Logging.log("Adding test permission");
|
||||||
description: "Permission just for testing purposes",
|
perm = Permission.new({
|
||||||
client: c._id
|
_id: new ObjectID("507f1f77bcf86cd799439011"),
|
||||||
});
|
name: "TestPerm",
|
||||||
|
description: "Permission just for testing purposes",
|
||||||
await (await (Permission as any)._collection).insertOne(perm);
|
client: c._id,
|
||||||
|
});
|
||||||
// Permission.save(perm);
|
|
||||||
}
|
await (await (Permission as any)._collection).insertOne(perm);
|
||||||
|
|
||||||
let r = await RegCode.findOne({ token: "test" });
|
// Permission.save(perm);
|
||||||
if (!r) {
|
}
|
||||||
Logging.log("Adding test reg_code")
|
|
||||||
r = RegCode.new({
|
let r = await RegCode.findOne({ token: "test" });
|
||||||
token: "test",
|
if (!r) {
|
||||||
valid: true,
|
Logging.log("Adding test reg_code");
|
||||||
validTill: moment().add("1", "year").toDate()
|
r = RegCode.new({
|
||||||
})
|
token: "test",
|
||||||
await RegCode.save(r);
|
valid: true,
|
||||||
}
|
validTill: moment().add("1", "year").toDate(),
|
||||||
|
});
|
||||||
let t = await TwoFactor.findOne({ user: u._id, type: 0 })
|
await RegCode.save(r);
|
||||||
if (!t) {
|
}
|
||||||
t = TwoFactor.new({
|
|
||||||
user: u._id,
|
let t = await TwoFactor.findOne({ user: u._id, type: 0 });
|
||||||
type: 0,
|
if (!t) {
|
||||||
valid: true,
|
t = TwoFactor.new({
|
||||||
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
user: u._id,
|
||||||
expires: null
|
type: 0,
|
||||||
})
|
valid: true,
|
||||||
TwoFactor.save(t);
|
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
||||||
}
|
expires: null,
|
||||||
|
});
|
||||||
let login_token = await LoginToken.findOne({ token: "test01" });
|
await TwoFactor.save(t);
|
||||||
if (login_token)
|
}
|
||||||
await LoginToken.delete(login_token);
|
|
||||||
|
let login_token = await LoginToken.findOne({ token: "test01" });
|
||||||
login_token = LoginToken.new({
|
if (login_token) await LoginToken.delete(login_token);
|
||||||
browser: "DEMO",
|
|
||||||
ip: "10.0.0.1",
|
login_token = LoginToken.new({
|
||||||
special: false,
|
browser: "DEMO",
|
||||||
token: "test01",
|
ip: "10.0.0.1",
|
||||||
valid: true,
|
special: false,
|
||||||
validTill: moment().add("10", "years").toDate(),
|
token: "test01",
|
||||||
user: u._id,
|
valid: true,
|
||||||
validated: true
|
validTill: moment().add("10", "years").toDate(),
|
||||||
});
|
user: u._id,
|
||||||
await LoginToken.save(login_token);
|
validated: true,
|
||||||
|
});
|
||||||
let special_token = await LoginToken.findOne({ token: "test02" });
|
await LoginToken.save(login_token);
|
||||||
if (special_token)
|
|
||||||
await LoginToken.delete(special_token);
|
let special_token = await LoginToken.findOne({ token: "test02" });
|
||||||
|
if (special_token) await LoginToken.delete(special_token);
|
||||||
special_token = LoginToken.new({
|
|
||||||
browser: "DEMO",
|
special_token = LoginToken.new({
|
||||||
ip: "10.0.0.1",
|
browser: "DEMO",
|
||||||
special: true,
|
ip: "10.0.0.1",
|
||||||
token: "test02",
|
special: true,
|
||||||
valid: true,
|
token: "test02",
|
||||||
validTill: moment().add("10", "years").toDate(),
|
valid: true,
|
||||||
user: u._id,
|
validTill: moment().add("10", "years").toDate(),
|
||||||
validated: true
|
user: u._id,
|
||||||
});
|
validated: true,
|
||||||
await LoginToken.save(special_token);
|
});
|
||||||
|
await LoginToken.save(special_token);
|
||||||
|
|
||||||
// setInterval(() => {
|
// setInterval(() => {
|
||||||
// let code = speakeasy.totp({
|
// let code = speakeasy.totp({
|
||||||
// secret: t.data,
|
// secret: t.data,
|
||||||
// encoding: "base32"
|
// encoding: "base32"
|
||||||
// })
|
// })
|
||||||
// Logging.debug("OTC Code is:", code);
|
// Logging.debug("OTC Code is:", code);
|
||||||
// }, 1000)
|
// }, 1000)
|
||||||
}
|
|
||||||
|
console.log("Finished adding test data")
|
||||||
|
}
|
8
Backend/src/views/admin.ts
Normal file
8
Backend/src/views/admin.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { __ as i__ } from "i18n";
|
||||||
|
import config from "../config";
|
||||||
|
import * as viewsv1 from "@hibas123/openauth-views-v1";
|
||||||
|
|
||||||
|
export default function GetAdminPage(__: typeof i__): string {
|
||||||
|
let data = {};
|
||||||
|
return viewsv1.admin(config.core.dev)(data, { helpers: { i18n: __ } });
|
||||||
|
}
|
22
Backend/src/views/authorize.ts
Normal file
22
Backend/src/views/authorize.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { __ as i__ } from "i18n";
|
||||||
|
import config from "../config";
|
||||||
|
import * as viewsv1 from "@hibas123/openauth-views-v1";
|
||||||
|
|
||||||
|
export default function GetAuthPage(
|
||||||
|
__: typeof i__,
|
||||||
|
appname: string,
|
||||||
|
scopes: { name: string; description: string; logo: string }[]
|
||||||
|
): string {
|
||||||
|
|
||||||
|
return viewsv1.authorize(config.core.dev)(
|
||||||
|
{
|
||||||
|
title: __("Authorize %s", appname),
|
||||||
|
information: __(
|
||||||
|
"By clicking on ALLOW, you allow this app to access the requested recources."
|
||||||
|
),
|
||||||
|
scopes: scopes,
|
||||||
|
// request: request
|
||||||
|
},
|
||||||
|
{ helpers: { i18n: __ } }
|
||||||
|
);
|
||||||
|
}
|
9
Backend/src/views/register.ts
Normal file
9
Backend/src/views/register.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { __ as i__ } from "i18n";
|
||||||
|
import config from "../config";
|
||||||
|
import * as viewsv1 from "@hibas123/openauth-views-v1";
|
||||||
|
|
||||||
|
export default function GetRegistrationPage(__: typeof i__): string {
|
||||||
|
let data = {};
|
||||||
|
return viewsv1.register(config.core.dev)(data, { helpers: { i18n: __ } });
|
||||||
|
}
|
||||||
|
|
115
Backend/src/views/views.ts
Normal file
115
Backend/src/views/views.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
IRouter,
|
||||||
|
Request,
|
||||||
|
RequestHandler,
|
||||||
|
Router,
|
||||||
|
static as ServeStatic,
|
||||||
|
} from "express";
|
||||||
|
import * as Handlebars from "handlebars";
|
||||||
|
import * as moment from "moment";
|
||||||
|
import { GetUserMiddleware, UserMiddleware } from "../api/middlewares/user";
|
||||||
|
import GetAuthRoute from "../api/oauth/auth";
|
||||||
|
import config from "../config";
|
||||||
|
import { HttpStatusCode } from "../helper/request_error";
|
||||||
|
import GetAdminPage from "./admin";
|
||||||
|
import GetRegistrationPage from "./register";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
const viewsv2_location = path.join(path.dirname(require.resolve("@hibas123/openauth-views-v2")), "build");
|
||||||
|
|
||||||
|
|
||||||
|
Handlebars.registerHelper("appname", () => config.core.name);
|
||||||
|
|
||||||
|
const cacheTime = !config.core.dev
|
||||||
|
? moment.duration(1, "month").asSeconds()
|
||||||
|
: 1000;
|
||||||
|
|
||||||
|
const addCache: RequestHandler = (req, res, next) => {
|
||||||
|
res.setHeader("cache-control", "public, max-age=" + cacheTime);
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ViewRouter: IRouter = Router();
|
||||||
|
ViewRouter.get("/", 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.use(
|
||||||
|
"/login",
|
||||||
|
addCache,
|
||||||
|
ServeStatic(path.join(viewsv2_location, "login"), { cacheControl: false })
|
||||||
|
);
|
||||||
|
|
||||||
|
ViewRouter.use(
|
||||||
|
"/user",
|
||||||
|
addCache,
|
||||||
|
ServeStatic(path.join(viewsv2_location, "user"), { cacheControl: false })
|
||||||
|
);
|
||||||
|
|
||||||
|
ViewRouter.get("/code", (req, res) => {
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
if (req.query.error) res.send("Some error occured: " + req.query.error);
|
||||||
|
else res.send(`Your code is: ${req.query.code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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", GetAuthRoute(true));
|
||||||
|
|
||||||
|
ViewRouter.use(
|
||||||
|
"/popup",
|
||||||
|
GetUserMiddleware(false, false),
|
||||||
|
addCache,
|
||||||
|
ServeStatic(path.join(viewsv2_location, "popup"), { cacheControl: false })
|
||||||
|
);
|
||||||
|
|
||||||
|
// ViewRouter.get("/popup", UserMiddleware, (req, res) => {
|
||||||
|
// res.send(GetPopupPage(req.__));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (config.core.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. And a lot of more Text, because why not? This will not stop, till it is multiple lines long and maybe kill the layout, so keep reading as long as you like, but I promise it will get boring after some time. So this should be enougth.",
|
||||||
|
// 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;
|
122
Backend/src/web.ts
Normal file
122
Backend/src/web.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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 ApiRouter from "./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 as any).language || ""
|
||||||
|
} ${resColor}${res.statusCode}\x1b[0m - ${time}ms`
|
||||||
|
);
|
||||||
|
res.removeListener("finish", listener);
|
||||||
|
};
|
||||||
|
res.on("finish", listener);
|
||||||
|
setTimeout(() => {
|
||||||
|
to = true;
|
||||||
|
listener();
|
||||||
|
}, 2000);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.use(
|
||||||
|
compression({
|
||||||
|
filter: (req, res) => {
|
||||||
|
if (req.headers["x-no-compression"]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return compression.filter(req, res);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
error = new RequestError(
|
||||||
|
error.message,
|
||||||
|
error.status || HttpStatusCode.INTERNAL_SERVER_ERROR,
|
||||||
|
error.nolog || false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 500 && !(<any>error).nolog) {
|
||||||
|
Logging.error(error);
|
||||||
|
} else {
|
||||||
|
Logging.log("Responded with Error", error.status, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.accepts(["json"])) {
|
||||||
|
res.json_status = error.status || 500;
|
||||||
|
res.json({
|
||||||
|
error: error.message,
|
||||||
|
status: error.status || 500,
|
||||||
|
additional: error.additional,
|
||||||
|
});
|
||||||
|
} else res.status(error.status || 500).send(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
17
Backend/tsconfig.json
Normal file
17
Backend/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
38
Dockerfile
38
Dockerfile
@ -1,33 +1,31 @@
|
|||||||
FROM node:12
|
FROM node:18-alpine
|
||||||
|
|
||||||
LABEL maintainer="Fabian Stamm <dev@fabianstamm.de>"
|
LABEL maintainer="Fabian Stamm <dev@fabianstamm.de>"
|
||||||
|
|
||||||
# RUN apt-get update
|
|
||||||
|
|
||||||
# # for https
|
|
||||||
# RUN apt-get install -yyq ca-certificates
|
|
||||||
# # install libraries
|
|
||||||
# RUN apt-get install -yyq libappindicator1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6
|
|
||||||
# # tools
|
|
||||||
# RUN apt-get install -yyq gconf-service lsb-release wget xdg-utils
|
|
||||||
# # and fonts
|
|
||||||
# RUN apt-get install -yyq fonts-liberation
|
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app
|
RUN mkdir -p /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY ["package.json", "package-lock.json", "tsconfig.json", "/usr/src/app/"]
|
# COPY ["package.json", "yarn.lock", ".yarnrc.yml", "/usr/src/app/"]
|
||||||
|
# COPY .yarn /usr/src/app/.yarn
|
||||||
|
# COPY Backend /usr/src/app/Backend
|
||||||
|
# COPY Frontend /usr/src/app/Frontend
|
||||||
|
# COPY FrontendLegacy /usr/src/app/FrontendLegacy
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
COPY . /usr/src/app
|
||||||
|
|
||||||
RUN npm install
|
# RUN rm -rf /usr/src/app/Backend/node_modules &&\
|
||||||
|
# rm -rf /usr/src/app/Frontend/node_modules &&\
|
||||||
|
# rm -rf /usr/src/app/FrontendLegacy/node_modules &&\
|
||||||
|
# rm -rf /usr/src/app/Backend/logs &&\
|
||||||
|
# rm -rf /usr/src/app/Backend/keys
|
||||||
|
|
||||||
COPY lib/ /usr/src/app/lib
|
RUN yarn install
|
||||||
COPY views/out /usr/src/app/views/out/
|
RUN yarn build
|
||||||
COPY views_repo/build /usr/src/app/views_repo/build
|
|
||||||
|
RUN ln -s /usr/src/app/logs /usr/src/app/Backend/logs && ln -s /usr/src/app/keys /usr/src/app/Backend/keys
|
||||||
|
|
||||||
VOLUME [ "/usr/src/app/logs", "/usr/src/app/keys"]
|
VOLUME [ "/usr/src/app/logs", "/usr/src/app/keys"]
|
||||||
|
|
||||||
EXPOSE 3004/tcp
|
EXPOSE 3004/tcp
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
WORKDIR /usr/src/app/Backend
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
|
11
Frontend/.editorconfig
Normal file
11
Frontend/.editorconfig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 3
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.svelte]
|
||||||
|
indent_size = 2
|
8
Frontend/.gitignore
vendored
Normal file
8
Frontend/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
public/bundle.*
|
||||||
|
yarn.lock
|
||||||
|
.rpt2_cache
|
||||||
|
build/
|
||||||
|
build.js
|
||||||
|
*.old
|
68
Frontend/README.md
Normal file
68
Frontend/README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
_Psst <14>looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# svelte app
|
||||||
|
|
||||||
|
This is a project template for [Svelte](https://svelte.technology) apps. It lives at https://github.com/sveltejs/template.
|
||||||
|
|
||||||
|
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g degit # you only need to do this once
|
||||||
|
|
||||||
|
degit sveltejs/template svelte-app
|
||||||
|
cd svelte-app
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note that you will need to have [Node.js](https://nodejs.org) installed._
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
Install the dependencies...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd svelte-app
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
...then start [Rollup](https://rollupjs.org):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||||
|
|
||||||
|
## Deploying to the web
|
||||||
|
|
||||||
|
### With [now](https://zeit.co/now)
|
||||||
|
|
||||||
|
Install `now` if you haven't already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g now
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, from within your project folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
now
|
||||||
|
```
|
||||||
|
|
||||||
|
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
|
||||||
|
|
||||||
|
### With [surge](https://surge.sh/)
|
||||||
|
|
||||||
|
Install `surge` if you haven't already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g surge
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, from within your project folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
surge public
|
||||||
|
```
|
36
Frontend/package.json
Normal file
36
Frontend/package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "@hibas123/openauth-views-v2",
|
||||||
|
"main": "index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-html": "^1.0.2",
|
||||||
|
"@rollup/plugin-image": "^3.0.2",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
|
"@tsconfig/svelte": "^4.0.1",
|
||||||
|
"@types/cleave.js": "^1.4.7",
|
||||||
|
"esbuild": "^0.17.15",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"postcss-import": "^15.1.0",
|
||||||
|
"postcss-url": "^10.1.3",
|
||||||
|
"rollup": "^3.20.2",
|
||||||
|
"rollup-plugin-esbuild": "^5.0.0",
|
||||||
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-sizes": "^1.0.5",
|
||||||
|
"rollup-plugin-svelte": "^7.1.4",
|
||||||
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
|
"svelte": "^3.58.0",
|
||||||
|
"svelte-preprocess": "^5.0.3",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"build": "rollup -c rollup.config.mjs ",
|
||||||
|
"dev": "rollup -c rollup.config.mjs -w"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hibas123/theme": "^2.0.6",
|
||||||
|
"@hibas123/utils": "^2.2.18",
|
||||||
|
"cleave.js": "^1.6.0",
|
||||||
|
"what-the-pack": "^2.0.3"
|
||||||
|
}
|
||||||
|
}
|
3
Frontend/postcss.config.js
Normal file
3
Frontend/postcss.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [],
|
||||||
|
};
|
123
Frontend/rollup.config.mjs
Normal file
123
Frontend/rollup.config.mjs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import svelte from "rollup-plugin-svelte";
|
||||||
|
import esbuild from "rollup-plugin-esbuild";
|
||||||
|
import html from "@rollup/plugin-html";
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import image from "@rollup/plugin-image";
|
||||||
|
import sizes from "rollup-plugin-sizes";
|
||||||
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
|
import postcss from "rollup-plugin-postcss";
|
||||||
|
import livereload from "rollup-plugin-livereload";
|
||||||
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
|
|
||||||
|
const VIEWS = ["home", "login", "popup", "user"];
|
||||||
|
|
||||||
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
|
const htmlTemplate = ({ attributes, meta, files, publicPath, title }) => {
|
||||||
|
const makeHtmlAttributes = (attributes) => {
|
||||||
|
if (!attributes) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(attributes);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
return keys.reduce(
|
||||||
|
(result, key) => (result += ` ${key}="${attributes[key]}"`),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const scripts = (files.js || [])
|
||||||
|
.map(({ fileName }) => {
|
||||||
|
const attrs = makeHtmlAttributes(attributes.script);
|
||||||
|
return `<script src="${publicPath}${fileName}"${attrs}></script>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const links = (files.css || [])
|
||||||
|
.map(({ fileName }) => {
|
||||||
|
const attrs = makeHtmlAttributes(attributes.link);
|
||||||
|
return `<link href="${publicPath}${fileName}" rel="stylesheet"${attrs}>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const metas = meta
|
||||||
|
.map((input) => {
|
||||||
|
const attrs = makeHtmlAttributes(input);
|
||||||
|
return `<meta${attrs}>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!doctype html>
|
||||||
|
<html${makeHtmlAttributes(attributes.html)}>
|
||||||
|
<head>
|
||||||
|
${metas}
|
||||||
|
<title>${title}</title>
|
||||||
|
<link rel="stylesheet" href="bundle.css"/>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto"/>
|
||||||
|
${links}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${scripts}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VIEWS.map((view) => ({
|
||||||
|
input: `src/pages/${view}/main.ts`,
|
||||||
|
output: [
|
||||||
|
dev
|
||||||
|
? {
|
||||||
|
file: `build/${view}/bundle.js`,
|
||||||
|
format: "iife",
|
||||||
|
sourcemap: true,
|
||||||
|
name: view,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
file: `build/${view}/bundle.min.js`,
|
||||||
|
format: "iife",
|
||||||
|
name: view,
|
||||||
|
plugins: [
|
||||||
|
esbuild({
|
||||||
|
minify: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
emitCss: true,
|
||||||
|
preprocess: sveltePreprocess({}),
|
||||||
|
}),
|
||||||
|
esbuild({ sourceMap: dev }),
|
||||||
|
html({
|
||||||
|
title: view,
|
||||||
|
attributes: {
|
||||||
|
html: { lang: "en" },
|
||||||
|
},
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: "viewport",
|
||||||
|
content: "width=device-width",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
template: htmlTemplate,
|
||||||
|
}),
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ["svelte"],
|
||||||
|
exportConditions: ["svelte"],
|
||||||
|
}),
|
||||||
|
image(),
|
||||||
|
sizes(),
|
||||||
|
visualizer({
|
||||||
|
filename: `build/stats/${view}.html`,
|
||||||
|
title: `Rullup bundle for ${view}`,
|
||||||
|
}),
|
||||||
|
postcss({
|
||||||
|
extract: `bundle.css`, //TODO: Check if it should be enabled
|
||||||
|
// inject: true,
|
||||||
|
}),
|
||||||
|
// dev && livereload(),
|
||||||
|
],
|
||||||
|
}));
|
90
Frontend/src/components/HoveringContentBox.svelte
Normal file
90
Frontend/src/components/HoveringContentBox.svelte
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// import { Tile } from "carbon-components-svelte";
|
||||||
|
|
||||||
|
export let title: string;
|
||||||
|
export let loading = false;
|
||||||
|
export let hide = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
padding-top: 2.5rem;
|
||||||
|
|
||||||
|
min-height: calc(100px + 2.5rem);
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
margin: -4.8rem auto 0 auto;
|
||||||
|
max-width: 250px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* padding: 5px 20px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container > h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
padding: 2em;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 380px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading_container {
|
||||||
|
filter: blur(1px) opacity(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader_container {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="card-elevated container">
|
||||||
|
<!-- <div class="container card"> -->
|
||||||
|
<div class="card elv-8 title-container">
|
||||||
|
<h1 style="margin:0">{title}</h1>
|
||||||
|
</div>
|
||||||
|
{#if loading}
|
||||||
|
<div class="loader_container">
|
||||||
|
<div class="loader_box">
|
||||||
|
<div class="loader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="content-container" class:loading_container={loading}>
|
||||||
|
{#if !(loading && hide)}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- </div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
15
Frontend/src/components/theme/Theme.svelte
Normal file
15
Frontend/src/components/theme/Theme.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// import { onMount, afterUpdate, setContext } from "svelte";
|
||||||
|
// import { writable, derived } from "svelte/store";
|
||||||
|
|
||||||
|
// type Theme = "white" | "g10" | "g90" | "g100";
|
||||||
|
|
||||||
|
// export let persist: boolean = false;
|
||||||
|
// export let persistKey: string = "theme";
|
||||||
|
export let dark = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class={dark ? 'dark-theme' : 'light-theme'}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
42
Frontend/src/components/theme/index.ts
Normal file
42
Frontend/src/components/theme/index.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import "@hibas123/theme/out/base.css";
|
||||||
|
import "./theme.css";
|
||||||
|
import { default as Theme } from "./Theme.svelte";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const elements = new WeakSet();
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".floating>input")
|
||||||
|
.forEach((e: HTMLInputElement) => {
|
||||||
|
if (elements.has(e)) return;
|
||||||
|
elements.add(e);
|
||||||
|
|
||||||
|
function checkState() {
|
||||||
|
console.log("Check State");
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start observing the target node for configured mutations
|
||||||
|
observer.observe(window.document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
check();
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default Theme;
|
251
Frontend/src/components/theme/theme.css
Normal file
251
Frontend/src/components/theme/theme.css
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
:root {
|
||||||
|
--primary: #1e88e5;
|
||||||
|
--mdc-theme-primary: var(--primary);
|
||||||
|
--mdc-theme-primary-bg: var(--mdc-theme--primary);
|
||||||
|
--mdc-theme-on-primary: white;
|
||||||
|
--error: #ff2f00;
|
||||||
|
--border-color: #ababab;
|
||||||
|
|
||||||
|
--default-font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "Roboto", "Helvetica", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
color: #636363;
|
||||||
|
position: relative;
|
||||||
|
background: #eee;
|
||||||
|
height: 100%;
|
||||||
|
font-size: var(--default-font-size);
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
min-height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating > input {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 10px 10px 10px 5px;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: block;
|
||||||
|
background: #fafafa;
|
||||||
|
background: unset;
|
||||||
|
color: #636363;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
/* border-bottom: 1px solid #757575; */
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: -0.75em;
|
||||||
|
transform: scale(0.75);
|
||||||
|
left: -2px;
|
||||||
|
/* font-size: 14px; */
|
||||||
|
color: var(--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: var(--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: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
width: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
margin: 2rem;
|
||||||
|
padding: 0 1em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-width: 0;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
background-color: #cccccc;
|
||||||
|
color: #ecf0f1;
|
||||||
|
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover,
|
||||||
|
.btn:focus {
|
||||||
|
filter: brightness(90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn > * {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn span {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:before {
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
background-color: rgba(236, 240, 241, 0.3);
|
||||||
|
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
-moz-transform: translate(-50%, -50%);
|
||||||
|
-ms-transform: translate(-50%, -50%);
|
||||||
|
-o-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active:before {
|
||||||
|
width: 120%;
|
||||||
|
padding-top: 120%;
|
||||||
|
|
||||||
|
transition: width 0.2s ease-out, padding-top 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader_box {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 var(--primary);
|
||||||
|
border-color: var(--primary) transparent var(--primary) transparent;
|
||||||
|
animation: loader 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
20
Frontend/src/helper/cookie.ts
Normal file
20
Frontend/src/helper/cookie.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export function setCookie(cname: string, cvalue: string, exdate: string) {
|
||||||
|
const expires = exdate ? `;expires=${exdate}` : "";
|
||||||
|
document.cookie = `${cname}=${cvalue}${expires};path=/;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookie(cname: string) {
|
||||||
|
const name = cname + "=";
|
||||||
|
const dc = decodeURIComponent(document.cookie);
|
||||||
|
const ca = dc.split(";");
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) == " ") {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
53
Frontend/src/helper/request.ts
Normal file
53
Frontend/src/helper/request.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { getCookie } from "./cookie";
|
||||||
|
|
||||||
|
const baseURL = "";
|
||||||
|
|
||||||
|
export default async function request(
|
||||||
|
endpoint: string,
|
||||||
|
parameters: { [key: string]: string } = {},
|
||||||
|
method: "GET" | "POST" | "DELETE" | "PUT" = "GET",
|
||||||
|
body?: any,
|
||||||
|
authInParam = false,
|
||||||
|
redirect = false
|
||||||
|
) {
|
||||||
|
let pairs = [];
|
||||||
|
|
||||||
|
if (authInParam) {
|
||||||
|
parameters.login = getCookie("login");
|
||||||
|
parameters.special = getCookie("special");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in parameters) {
|
||||||
|
pairs.push(key + "=" + parameters[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = endpoint;
|
||||||
|
if (pairs.length > 0) {
|
||||||
|
url += "?" + pairs.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(baseURL + url, {
|
||||||
|
method,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((e) => {
|
||||||
|
if (e.status !== 200) throw new Error(e.statusText);
|
||||||
|
return e.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
if (redirect && data.additional && data.additional.auth) {
|
||||||
|
let state = btoa(
|
||||||
|
window.location.pathname + window.location.hash
|
||||||
|
);
|
||||||
|
window.location.href = `/login?state=${state}&base64=true`;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(data.error));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
484
Frontend/src/helper/sha512.js
Normal file
484
Frontend/src/helper/sha512.js
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
var b;
|
||||||
|
if (!(b = t)) {
|
||||||
|
var w = Math,
|
||||||
|
y = {},
|
||||||
|
B = (y.p = {}),
|
||||||
|
aa = function () {},
|
||||||
|
C = (B.A = {
|
||||||
|
extend: function (o) {
|
||||||
|
aa.prototype = this;
|
||||||
|
var _ = new aa();
|
||||||
|
return o && _.u(o), (_.z = this), _;
|
||||||
|
},
|
||||||
|
create: function () {
|
||||||
|
var o = this.extend();
|
||||||
|
return o.h.apply(o, arguments), o;
|
||||||
|
},
|
||||||
|
h: function () {},
|
||||||
|
u: function (o) {
|
||||||
|
for (var _ in o) o.hasOwnProperty(_) && (this[_] = o[_]);
|
||||||
|
o.hasOwnProperty("toString") && (this.toString = o.toString);
|
||||||
|
},
|
||||||
|
e: function () {
|
||||||
|
return this.z.extend(this);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
D = (B.i = C.extend({
|
||||||
|
h: function (o, _) {
|
||||||
|
(o = this.d = o || []), (this.c = void 0 == _ ? 4 * o.length : _);
|
||||||
|
},
|
||||||
|
toString: function (o) {
|
||||||
|
return (o || ba).stringify(this);
|
||||||
|
},
|
||||||
|
concat: function (o) {
|
||||||
|
var _ = this.d,
|
||||||
|
Da = o.d,
|
||||||
|
Ea = this.c,
|
||||||
|
o = o.c;
|
||||||
|
if ((this.t(), Ea % 4))
|
||||||
|
for (var Fa = 0; Fa < o; Fa++)
|
||||||
|
_[(Ea + Fa) >>> 2] |=
|
||||||
|
(255 & (Da[Fa >>> 2] >>> (24 - 8 * (Fa % 4)))) <<
|
||||||
|
(24 - 8 * ((Ea + Fa) % 4));
|
||||||
|
else if (65535 < Da.length)
|
||||||
|
for (Fa = 0; Fa < o; Fa += 4) _[(Ea + Fa) >>> 2] = Da[Fa >>> 2];
|
||||||
|
else _.push.apply(_, Da);
|
||||||
|
return (this.c += o), this;
|
||||||
|
},
|
||||||
|
t: function () {
|
||||||
|
var o = this.d,
|
||||||
|
_ = this.c;
|
||||||
|
(o[_ >>> 2] &= 4294967295 << (32 - 8 * (_ % 4))),
|
||||||
|
(o.length = w.ceil(_ / 4));
|
||||||
|
},
|
||||||
|
e: function () {
|
||||||
|
var o = C.e.call(this);
|
||||||
|
return (o.d = this.d.slice(0)), o;
|
||||||
|
},
|
||||||
|
random: function (o) {
|
||||||
|
for (var _ = [], Da = 0; Da < o; Da += 4)
|
||||||
|
_.push(0 | (4294967296 * w.random()));
|
||||||
|
return D.create(_, o);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
ca = (y.O = {}),
|
||||||
|
ba = (ca.K = {
|
||||||
|
stringify: function (o) {
|
||||||
|
for (var Fa, _ = o.d, o = o.c, Da = [], Ea = 0; Ea < o; Ea++)
|
||||||
|
(Fa = 255 & (_[Ea >>> 2] >>> (24 - 8 * (Ea % 4)))),
|
||||||
|
Da.push((Fa >>> 4).toString(16)),
|
||||||
|
Da.push((15 & Fa).toString(16));
|
||||||
|
return Da.join("");
|
||||||
|
},
|
||||||
|
parse: function (o) {
|
||||||
|
for (var _ = o.length, Da = [], Ea = 0; Ea < _; Ea += 2)
|
||||||
|
Da[Ea >>> 3] |=
|
||||||
|
parseInt(o.substr(Ea, 2), 16) << (24 - 4 * (Ea % 8));
|
||||||
|
return D.create(Da, _ / 2);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
da = (ca.M = {
|
||||||
|
stringify: function (o) {
|
||||||
|
for (var _ = o.d, o = o.c, Da = [], Ea = 0; Ea < o; Ea++)
|
||||||
|
Da.push(
|
||||||
|
String.fromCharCode(
|
||||||
|
255 & (_[Ea >>> 2] >>> (24 - 8 * (Ea % 4)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Da.join("");
|
||||||
|
},
|
||||||
|
parse: function (o) {
|
||||||
|
for (var _ = o.length, Da = [], Ea = 0; Ea < _; Ea++)
|
||||||
|
Da[Ea >>> 2] |= (255 & o.charCodeAt(Ea)) << (24 - 8 * (Ea % 4));
|
||||||
|
return D.create(Da, _);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ea = (ca.N = {
|
||||||
|
stringify: function (o) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(da.stringify(o)));
|
||||||
|
} catch (_) {
|
||||||
|
throw Error("Malformed UTF-8 data");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parse: function (o) {
|
||||||
|
return da.parse(unescape(encodeURIComponent(o)));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ia = (B.I = C.extend({
|
||||||
|
reset: function () {
|
||||||
|
(this.g = D.create()), (this.j = 0);
|
||||||
|
},
|
||||||
|
l: function (o) {
|
||||||
|
"string" == typeof o && (o = ea.parse(o)),
|
||||||
|
this.g.concat(o),
|
||||||
|
(this.j += o.c);
|
||||||
|
},
|
||||||
|
m: function (o) {
|
||||||
|
var _ = this.g,
|
||||||
|
Da = _.d,
|
||||||
|
Ea = _.c,
|
||||||
|
Fa = this.n,
|
||||||
|
Ga = Ea / (4 * Fa),
|
||||||
|
Ga = o ? w.ceil(Ga) : w.max((0 | Ga) - this.r, 0),
|
||||||
|
o = Ga * Fa,
|
||||||
|
Ea = w.min(4 * o, Ea);
|
||||||
|
if (o) {
|
||||||
|
for (var Ha = 0; Ha < o; Ha += Fa) this.H(Da, Ha);
|
||||||
|
(Ha = Da.splice(0, o)), (_.c -= Ea);
|
||||||
|
}
|
||||||
|
return D.create(Ha, Ea);
|
||||||
|
},
|
||||||
|
e: function () {
|
||||||
|
var o = C.e.call(this);
|
||||||
|
return (o.g = this.g.e()), o;
|
||||||
|
},
|
||||||
|
r: 0,
|
||||||
|
}));
|
||||||
|
B.B = ia.extend({
|
||||||
|
h: function () {
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
ia.reset.call(this), this.q();
|
||||||
|
},
|
||||||
|
update: function (o) {
|
||||||
|
return this.l(o), this.m(), this;
|
||||||
|
},
|
||||||
|
o: function (o) {
|
||||||
|
return o && this.l(o), this.G(), this.f;
|
||||||
|
},
|
||||||
|
e: function () {
|
||||||
|
var o = ia.e.call(this);
|
||||||
|
return (o.f = this.f.e()), o;
|
||||||
|
},
|
||||||
|
n: 16,
|
||||||
|
D: function (o) {
|
||||||
|
return function (_, Da) {
|
||||||
|
return o.create(Da).o(_);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
F: function (o) {
|
||||||
|
return function (_, Da) {
|
||||||
|
return ja.J.create(o, Da).o(_);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var ja = (y.s = {});
|
||||||
|
b = y;
|
||||||
|
}
|
||||||
|
var t = b,
|
||||||
|
K = t,
|
||||||
|
ka = K.p,
|
||||||
|
la = ka.A,
|
||||||
|
va = ka.i,
|
||||||
|
K = (K.w = {});
|
||||||
|
(K.C = la.extend({
|
||||||
|
h: function (o, _) {
|
||||||
|
(this.a = o), (this.b = _);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
(K.i = la.extend({
|
||||||
|
h: function (o, _) {
|
||||||
|
(o = this.d = o || []), (this.c = void 0 == _ ? 8 * o.length : _);
|
||||||
|
},
|
||||||
|
v: function () {
|
||||||
|
for (var Fa, o = this.d, _ = o.length, Da = [], Ea = 0; Ea < _; Ea++)
|
||||||
|
(Fa = o[Ea]), Da.push(Fa.a), Da.push(Fa.b);
|
||||||
|
return va.create(Da, this.c);
|
||||||
|
},
|
||||||
|
e: function () {
|
||||||
|
for (
|
||||||
|
var o = la.e.call(this),
|
||||||
|
_ = (o.d = this.d.slice(0)),
|
||||||
|
Da = _.length,
|
||||||
|
Ea = 0;
|
||||||
|
Ea < Da;
|
||||||
|
Ea++
|
||||||
|
)
|
||||||
|
_[Ea] = _[Ea].e();
|
||||||
|
return o;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
function L() {
|
||||||
|
return wa.create.apply(wa, arguments);
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
var xa = t.p.B,
|
||||||
|
M = t.w,
|
||||||
|
wa = M.C,
|
||||||
|
ya = M.i,
|
||||||
|
M = t.s,
|
||||||
|
za = [
|
||||||
|
L(1116352408, 3609767458),
|
||||||
|
L(1899447441, 602891725),
|
||||||
|
L(3049323471, 3964484399),
|
||||||
|
L(3921009573, 2173295548),
|
||||||
|
L(961987163, 4081628472),
|
||||||
|
L(1508970993, 3053834265),
|
||||||
|
L(2453635748, 2937671579),
|
||||||
|
L(2870763221, 3664609560),
|
||||||
|
L(3624381080, 2734883394),
|
||||||
|
L(310598401, 1164996542),
|
||||||
|
L(607225278, 1323610764),
|
||||||
|
L(1426881987, 3590304994),
|
||||||
|
L(1925078388, 4068182383),
|
||||||
|
L(2162078206, 991336113),
|
||||||
|
L(2614888103, 633803317),
|
||||||
|
L(3248222580, 3479774868),
|
||||||
|
L(3835390401, 2666613458),
|
||||||
|
L(4022224774, 944711139),
|
||||||
|
L(264347078, 2341262773),
|
||||||
|
L(604807628, 2007800933),
|
||||||
|
L(770255983, 1495990901),
|
||||||
|
L(1249150122, 1856431235),
|
||||||
|
L(1555081692, 3175218132),
|
||||||
|
L(1996064986, 2198950837),
|
||||||
|
L(2554220882, 3999719339),
|
||||||
|
L(2821834349, 766784016),
|
||||||
|
L(2952996808, 2566594879),
|
||||||
|
L(3210313671, 3203337956),
|
||||||
|
L(3336571891, 1034457026),
|
||||||
|
L(3584528711, 2466948901),
|
||||||
|
L(113926993, 3758326383),
|
||||||
|
L(338241895, 168717936),
|
||||||
|
L(666307205, 1188179964),
|
||||||
|
L(773529912, 1546045734),
|
||||||
|
L(1294757372, 1522805485),
|
||||||
|
L(1396182291, 2643833823),
|
||||||
|
L(1695183700, 2343527390),
|
||||||
|
L(1986661051, 1014477480),
|
||||||
|
L(2177026350, 1206759142),
|
||||||
|
L(2456956037, 344077627),
|
||||||
|
L(2730485921, 1290863460),
|
||||||
|
L(2820302411, 3158454273),
|
||||||
|
L(3259730800, 3505952657),
|
||||||
|
L(3345764771, 106217008),
|
||||||
|
L(3516065817, 3606008344),
|
||||||
|
L(3600352804, 1432725776),
|
||||||
|
L(4094571909, 1467031594),
|
||||||
|
L(275423344, 851169720),
|
||||||
|
L(430227734, 3100823752),
|
||||||
|
L(506948616, 1363258195),
|
||||||
|
L(659060556, 3750685593),
|
||||||
|
L(883997877, 3785050280),
|
||||||
|
L(958139571, 3318307427),
|
||||||
|
L(1322822218, 3812723403),
|
||||||
|
L(1537002063, 2003034995),
|
||||||
|
L(1747873779, 3602036899),
|
||||||
|
L(1955562222, 1575990012),
|
||||||
|
L(2024104815, 1125592928),
|
||||||
|
L(2227730452, 2716904306),
|
||||||
|
L(2361852424, 442776044),
|
||||||
|
L(2428436474, 593698344),
|
||||||
|
L(2756734187, 3733110249),
|
||||||
|
L(3204031479, 2999351573),
|
||||||
|
L(3329325298, 3815920427),
|
||||||
|
L(3391569614, 3928383900),
|
||||||
|
L(3515267271, 566280711),
|
||||||
|
L(3940187606, 3454069534),
|
||||||
|
L(4118630271, 4000239992),
|
||||||
|
L(116418474, 1914138554),
|
||||||
|
L(174292421, 2731055270),
|
||||||
|
L(289380356, 3203993006),
|
||||||
|
L(460393269, 320620315),
|
||||||
|
L(685471733, 587496836),
|
||||||
|
L(852142971, 1086792851),
|
||||||
|
L(1017036298, 365543100),
|
||||||
|
L(1126000580, 2618297676),
|
||||||
|
L(1288033470, 3409855158),
|
||||||
|
L(1501505948, 4234509866),
|
||||||
|
L(1607167915, 987167468),
|
||||||
|
L(1816402316, 1246189591),
|
||||||
|
],
|
||||||
|
$ = [],
|
||||||
|
Aa = 0;
|
||||||
|
80 > Aa;
|
||||||
|
Aa++
|
||||||
|
)
|
||||||
|
$[Aa] = L();
|
||||||
|
(M = M.k = xa.extend({
|
||||||
|
q: function () {
|
||||||
|
this.f = ya.create([
|
||||||
|
L(1779033703, 4089235720),
|
||||||
|
L(3144134277, 2227873595),
|
||||||
|
L(1013904242, 4271175723),
|
||||||
|
L(2773480762, 1595750129),
|
||||||
|
L(1359893119, 2917565137),
|
||||||
|
L(2600822924, 725511199),
|
||||||
|
L(528734635, 4215389547),
|
||||||
|
L(1541459225, 327033209),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
H: function (o, _) {
|
||||||
|
for (
|
||||||
|
var qb,
|
||||||
|
Da = this.f.d,
|
||||||
|
Ea = Da[0],
|
||||||
|
Fa = Da[1],
|
||||||
|
Ga = Da[2],
|
||||||
|
Ha = Da[3],
|
||||||
|
Ia = Da[4],
|
||||||
|
Ja = Da[5],
|
||||||
|
Ka = Da[6],
|
||||||
|
Da = Da[7],
|
||||||
|
La = Ea.a,
|
||||||
|
Ma = Ea.b,
|
||||||
|
Na = Fa.a,
|
||||||
|
Oa = Fa.b,
|
||||||
|
Pa = Ga.a,
|
||||||
|
Qa = Ga.b,
|
||||||
|
Ra = Ha.a,
|
||||||
|
Sa = Ha.b,
|
||||||
|
Ta = Ia.a,
|
||||||
|
Ua = Ia.b,
|
||||||
|
Va = Ja.a,
|
||||||
|
Wa = Ja.b,
|
||||||
|
Xa = Ka.a,
|
||||||
|
Ya = Ka.b,
|
||||||
|
Za = Da.a,
|
||||||
|
$a = Da.b,
|
||||||
|
_a = La,
|
||||||
|
ab = Ma,
|
||||||
|
bb = Na,
|
||||||
|
cb = Oa,
|
||||||
|
db = Pa,
|
||||||
|
eb = Qa,
|
||||||
|
fb = Ra,
|
||||||
|
gb = Sa,
|
||||||
|
hb = Ta,
|
||||||
|
ib = Ua,
|
||||||
|
jb = Va,
|
||||||
|
kb = Wa,
|
||||||
|
lb = Xa,
|
||||||
|
mb = Ya,
|
||||||
|
nb = Za,
|
||||||
|
ob = $a,
|
||||||
|
pb = 0;
|
||||||
|
80 > pb;
|
||||||
|
pb++
|
||||||
|
) {
|
||||||
|
if (((qb = $[pb]), 16 > pb))
|
||||||
|
var rb = (qb.a = 0 | o[_ + 2 * pb]),
|
||||||
|
sb = (qb.b = 0 | o[_ + 2 * pb + 1]);
|
||||||
|
else {
|
||||||
|
var rb = $[pb - 15],
|
||||||
|
sb = rb.a,
|
||||||
|
tb = rb.b,
|
||||||
|
rb =
|
||||||
|
((tb << 31) | (sb >>> 1)) ^
|
||||||
|
((tb << 24) | (sb >>> 8)) ^
|
||||||
|
(sb >>> 7),
|
||||||
|
tb =
|
||||||
|
((sb << 31) | (tb >>> 1)) ^
|
||||||
|
((sb << 24) | (tb >>> 8)) ^
|
||||||
|
((sb << 25) | (tb >>> 7)),
|
||||||
|
ub = $[pb - 2],
|
||||||
|
sb = ub.a,
|
||||||
|
vb = ub.b,
|
||||||
|
ub =
|
||||||
|
((vb << 13) | (sb >>> 19)) ^
|
||||||
|
((sb << 3) | (vb >>> 29)) ^
|
||||||
|
(sb >>> 6),
|
||||||
|
vb =
|
||||||
|
((sb << 13) | (vb >>> 19)) ^
|
||||||
|
((vb << 3) | (sb >>> 29)) ^
|
||||||
|
((sb << 26) | (vb >>> 6)),
|
||||||
|
sb = $[pb - 7],
|
||||||
|
wb = sb.a,
|
||||||
|
xb = $[pb - 16],
|
||||||
|
yb = xb.a,
|
||||||
|
xb = xb.b,
|
||||||
|
sb = tb + sb.b,
|
||||||
|
rb = rb + wb + (sb >>> 0 < tb >>> 0 ? 1 : 0),
|
||||||
|
sb = sb + vb,
|
||||||
|
rb = rb + ub + (sb >>> 0 < vb >>> 0 ? 1 : 0),
|
||||||
|
sb = sb + xb,
|
||||||
|
rb = rb + yb + (sb >>> 0 < xb >>> 0 ? 1 : 0);
|
||||||
|
(qb.a = rb), (qb.b = sb);
|
||||||
|
}
|
||||||
|
var wb = (hb & jb) ^ (~hb & lb),
|
||||||
|
xb = (ib & kb) ^ (~ib & mb),
|
||||||
|
qb = (_a & bb) ^ (_a & db) ^ (bb & db),
|
||||||
|
tb =
|
||||||
|
((ab << 4) | (_a >>> 28)) ^
|
||||||
|
((_a << 30) | (ab >>> 2)) ^
|
||||||
|
((_a << 25) | (ab >>> 7)),
|
||||||
|
ub =
|
||||||
|
((_a << 4) | (ab >>> 28)) ^
|
||||||
|
((ab << 30) | (_a >>> 2)) ^
|
||||||
|
((ab << 25) | (_a >>> 7)),
|
||||||
|
vb = za[pb],
|
||||||
|
Ab = vb.a,
|
||||||
|
Bb = vb.b,
|
||||||
|
vb =
|
||||||
|
ob +
|
||||||
|
(((hb << 18) | (ib >>> 14)) ^
|
||||||
|
((hb << 14) | (ib >>> 18)) ^
|
||||||
|
((ib << 23) | (hb >>> 9))),
|
||||||
|
yb =
|
||||||
|
nb +
|
||||||
|
(((ib << 18) | (hb >>> 14)) ^
|
||||||
|
((ib << 14) | (hb >>> 18)) ^
|
||||||
|
((hb << 23) | (ib >>> 9))) +
|
||||||
|
(vb >>> 0 < ob >>> 0 ? 1 : 0),
|
||||||
|
vb = vb + xb,
|
||||||
|
yb = yb + wb + (vb >>> 0 < xb >>> 0 ? 1 : 0),
|
||||||
|
vb = vb + Bb,
|
||||||
|
yb = yb + Ab + (vb >>> 0 < Bb >>> 0 ? 1 : 0),
|
||||||
|
vb = vb + sb,
|
||||||
|
yb = yb + rb + (vb >>> 0 < sb >>> 0 ? 1 : 0),
|
||||||
|
sb = ub + ((ab & cb) ^ (ab & eb) ^ (cb & eb)),
|
||||||
|
qb = tb + qb + (sb >>> 0 < ub >>> 0 ? 1 : 0),
|
||||||
|
nb = lb,
|
||||||
|
ob = mb,
|
||||||
|
lb = jb,
|
||||||
|
mb = kb,
|
||||||
|
jb = hb,
|
||||||
|
kb = ib,
|
||||||
|
ib = 0 | (gb + vb),
|
||||||
|
hb = 0 | (fb + yb + (ib >>> 0 < gb >>> 0 ? 1 : 0)),
|
||||||
|
fb = db,
|
||||||
|
gb = eb,
|
||||||
|
db = bb,
|
||||||
|
eb = cb,
|
||||||
|
bb = _a,
|
||||||
|
cb = ab,
|
||||||
|
ab = 0 | (vb + sb),
|
||||||
|
_a = 0 | (yb + qb + (ab >>> 0 < vb >>> 0 ? 1 : 0));
|
||||||
|
}
|
||||||
|
(Ma = Ea.b = 0 | (Ma + ab)),
|
||||||
|
(Ea.a = 0 | (La + _a + (Ma >>> 0 < ab >>> 0 ? 1 : 0))),
|
||||||
|
(Oa = Fa.b = 0 | (Oa + cb)),
|
||||||
|
(Fa.a = 0 | (Na + bb + (Oa >>> 0 < cb >>> 0 ? 1 : 0))),
|
||||||
|
(Qa = Ga.b = 0 | (Qa + eb)),
|
||||||
|
(Ga.a = 0 | (Pa + db + (Qa >>> 0 < eb >>> 0 ? 1 : 0))),
|
||||||
|
(Sa = Ha.b = 0 | (Sa + gb)),
|
||||||
|
(Ha.a = 0 | (Ra + fb + (Sa >>> 0 < gb >>> 0 ? 1 : 0))),
|
||||||
|
(Ua = Ia.b = 0 | (Ua + ib)),
|
||||||
|
(Ia.a = 0 | (Ta + hb + (Ua >>> 0 < ib >>> 0 ? 1 : 0))),
|
||||||
|
(Wa = Ja.b = 0 | (Wa + kb)),
|
||||||
|
(Ja.a = 0 | (Va + jb + (Wa >>> 0 < kb >>> 0 ? 1 : 0))),
|
||||||
|
(Ya = Ka.b = 0 | (Ya + mb)),
|
||||||
|
(Ka.a = 0 | (Xa + lb + (Ya >>> 0 < mb >>> 0 ? 1 : 0))),
|
||||||
|
($a = Da.b = 0 | ($a + ob)),
|
||||||
|
(Da.a = 0 | (Za + nb + ($a >>> 0 < ob >>> 0 ? 1 : 0)));
|
||||||
|
},
|
||||||
|
G: function () {
|
||||||
|
var o = this.g,
|
||||||
|
_ = o.d,
|
||||||
|
Da = 8 * this.j,
|
||||||
|
Ea = 8 * o.c;
|
||||||
|
(_[Ea >>> 5] |= 128 << (24 - (Ea % 32))),
|
||||||
|
(_[(((Ea + 128) >>> 10) << 5) + 31] = Da),
|
||||||
|
(o.c = 4 * _.length),
|
||||||
|
this.m(),
|
||||||
|
(this.f = this.f.v());
|
||||||
|
},
|
||||||
|
n: 32,
|
||||||
|
})),
|
||||||
|
(t.k = xa.D(M)),
|
||||||
|
(t.L = xa.F(M));
|
||||||
|
export default function sha512(o) {
|
||||||
|
return t.k(o) + "";
|
||||||
|
}
|
44
Frontend/src/pages/home/App.svelte
Normal file
44
Frontend/src/pages/home/App.svelte
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<style>
|
||||||
|
.main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<h1>Home Page</h1>
|
||||||
|
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>
|
||||||
|
OpenAuth is a Service to provide simple Authentication to a veriaty of
|
||||||
|
Applications. With a simple to use API and different Strategies, it can be
|
||||||
|
easily integrated into most Applications.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>QickLinks</h2>
|
||||||
|
<p>
|
||||||
|
If you want to manage your Account, click
|
||||||
|
<a href="user.html">here</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Applications using OpenAuth</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://ebook.stamm.me">EBook Store and Reader</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://notes.hibas123.de">
|
||||||
|
Secure and Simple Notes application
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
8
Frontend/src/pages/home/main.ts
Normal file
8
Frontend/src/pages/home/main.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import "../../components/theme";
|
||||||
|
import App from "./App.svelte";
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
124
Frontend/src/pages/login/App.svelte
Normal file
124
Frontend/src/pages/login/App.svelte
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
import Theme from "../../components/theme";
|
||||||
|
import HoveringContentBox from "../../components/HoveringContentBox.svelte";
|
||||||
|
import Api from "./api.ts";
|
||||||
|
import Credentials from "./Credentials.svelte";
|
||||||
|
import Redirect from "./Redirect.svelte";
|
||||||
|
import Twofactor from "./Twofactor.svelte";
|
||||||
|
|
||||||
|
const appname = "OpenAuth";
|
||||||
|
|
||||||
|
const states = {
|
||||||
|
credentials: 1,
|
||||||
|
twofactor: 3,
|
||||||
|
redirect: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = Api.getUsername();
|
||||||
|
let password = "";
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
let state = states.credentials;
|
||||||
|
|
||||||
|
function getButtonText(state) {
|
||||||
|
switch (state) {
|
||||||
|
case states.username:
|
||||||
|
return "Next";
|
||||||
|
case states.password:
|
||||||
|
return "Login";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: btnText = getButtonText(state);
|
||||||
|
|
||||||
|
let error;
|
||||||
|
|
||||||
|
// window.addEventListener("popstate", () => {
|
||||||
|
// state = history.state;
|
||||||
|
// })
|
||||||
|
|
||||||
|
function LoadRedirect() {
|
||||||
|
state = states.redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Loading() {
|
||||||
|
state = states.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt;
|
||||||
|
async function buttonClick() {
|
||||||
|
if (state === states.username) {
|
||||||
|
Loading();
|
||||||
|
let res = await Api.setUsername(username);
|
||||||
|
if (res.error) {
|
||||||
|
error = res.error;
|
||||||
|
LoadUsername();
|
||||||
|
} else {
|
||||||
|
LoadPassword();
|
||||||
|
}
|
||||||
|
} else if (state === states.password) {
|
||||||
|
Loading();
|
||||||
|
let res = await Api.setPassword(password);
|
||||||
|
if (res.error) {
|
||||||
|
error = res.error;
|
||||||
|
LoadPassword();
|
||||||
|
} else {
|
||||||
|
if (res.tfa) {
|
||||||
|
// TODO: Make TwoFactor UI/-s
|
||||||
|
} else {
|
||||||
|
LoadRedirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btnText = "Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRedirect() {
|
||||||
|
state = states.redirect;
|
||||||
|
// Show message to User and then redirect
|
||||||
|
setTimeout(() => Api.finish(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterCredentials() {
|
||||||
|
Object.keys(Api); // Some weird bug needs this???
|
||||||
|
|
||||||
|
if (Api.twofactor) {
|
||||||
|
state = states.twofactor;
|
||||||
|
} else {
|
||||||
|
startRedirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterTwoFactor() {
|
||||||
|
startRedirect();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<Theme>
|
||||||
|
<HoveringContentBox title="Login" {loading}>
|
||||||
|
<form action="JavaScript:void(0)">
|
||||||
|
{#if state === states.redirect}
|
||||||
|
<Redirect />
|
||||||
|
{:else if state === states.credentials}
|
||||||
|
<Credentials next={afterCredentials} setLoading={(s) => (loading = s)} />
|
||||||
|
{:else if state === states.twofactor}
|
||||||
|
<Twofactor finish={afterTwoFactor} setLoading={(s) => (loading = s)} />
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</HoveringContentBox>
|
||||||
|
<footer>
|
||||||
|
<p>Powered by {appname}</p>
|
||||||
|
</footer>
|
||||||
|
</Theme>
|
84
Frontend/src/pages/login/Credentials.svelte
Normal file
84
Frontend/src/pages/login/Credentials.svelte
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<script>
|
||||||
|
import Api from "./api.ts";
|
||||||
|
|
||||||
|
let error;
|
||||||
|
let password = "";
|
||||||
|
let username = Api.getUsername();
|
||||||
|
|
||||||
|
const states = {
|
||||||
|
username: 1,
|
||||||
|
password: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = states.username;
|
||||||
|
|
||||||
|
let salt;
|
||||||
|
|
||||||
|
export let setLoading;
|
||||||
|
export let next;
|
||||||
|
|
||||||
|
async function buttonClick() {
|
||||||
|
setLoading(true);
|
||||||
|
if (state === states.username) {
|
||||||
|
let res = await Api.setUsername(username);
|
||||||
|
if (res.error) {
|
||||||
|
error = res.error;
|
||||||
|
} else {
|
||||||
|
state = states.password;
|
||||||
|
error = undefined;
|
||||||
|
}
|
||||||
|
} else if (state === states.password) {
|
||||||
|
let res = await Api.setPassword(password);
|
||||||
|
if (res.error) {
|
||||||
|
error = res.error;
|
||||||
|
} else {
|
||||||
|
error = undefined;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error {
|
||||||
|
color: var(--error);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide-button {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if state === states.username}
|
||||||
|
<h3>Enter your Username or your E-Mail Address</h3>
|
||||||
|
<div class="floating group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
autofocus
|
||||||
|
bind:value={username} />
|
||||||
|
<span class="highlight" />
|
||||||
|
<span class="bar" />
|
||||||
|
<label>Username or E-Mail</label>
|
||||||
|
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<h3>Enter password for {username}</h3>
|
||||||
|
<div class="floating group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
autocomplete="password"
|
||||||
|
autofocus
|
||||||
|
bind:value={password} />
|
||||||
|
<span class="highlight" />
|
||||||
|
<span class="bar" />
|
||||||
|
<label>Password</label>
|
||||||
|
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button class="btn btn-primary wide-button" on:click={buttonClick}>Next</button>
|
99
Frontend/src/pages/login/Redirect.svelte
Normal file
99
Frontend/src/pages/login/Redirect.svelte
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<script>
|
||||||
|
// import {
|
||||||
|
// onMount,
|
||||||
|
// onDestroy
|
||||||
|
// } from "svelte";
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
|
||||||
|
const basetext = "Logged in. Redirecting";
|
||||||
|
let dots = 0;
|
||||||
|
|
||||||
|
$: text = basetext + ".".repeat(dots);
|
||||||
|
|
||||||
|
let iv;
|
||||||
|
onMount(() => {
|
||||||
|
console.log("Mounted");
|
||||||
|
iv = setInterval(() => {
|
||||||
|
dots++;
|
||||||
|
if (dots > 3) dots = 0;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
console.log("on Destroy");
|
||||||
|
clearInterval(iv);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.checkmark__circle {
|
||||||
|
stroke-dasharray: 166;
|
||||||
|
stroke-dashoffset: 166;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke: #7ac142;
|
||||||
|
fill: none;
|
||||||
|
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
margin: 10% auto;
|
||||||
|
box-shadow: inset 0px 0px 0px #7ac142;
|
||||||
|
animation: fill 0.4s ease-in-out 0.4s forwards,
|
||||||
|
scale 0.3s ease-in-out 0.9s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark__check {
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
stroke-dasharray: 48;
|
||||||
|
stroke-dashoffset: 48;
|
||||||
|
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stroke {
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale3d(1.1, 1.1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fill {
|
||||||
|
100% {
|
||||||
|
box-shadow: inset 0px 0px 0px 30px #7ac142;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale {
|
||||||
|
transform: scale(1.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="scale">
|
||||||
|
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
||||||
|
<circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" />
|
||||||
|
<path
|
||||||
|
class="checkmark__check"
|
||||||
|
fill="none"
|
||||||
|
d="M14.1 27.2l7.1 7.2 16.7-16.8" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- <div style="text-align: center;"> -->
|
||||||
|
<h3>{text}</h3>
|
||||||
|
<!-- </div> -->
|
104
Frontend/src/pages/login/Twofactor.svelte
Normal file
104
Frontend/src/pages/login/Twofactor.svelte
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import Api, { TFATypes } from "./api.ts";
|
||||||
|
import Icon from "./icons/Icon.svelte";
|
||||||
|
|
||||||
|
import OTCTwoFactor from "./twofactors/otc.svelte";
|
||||||
|
import PushTwoFactor from "./twofactors/push.svelte";
|
||||||
|
import U2FTwoFactor from "./twofactors/u2f.svelte";
|
||||||
|
|
||||||
|
const states = {
|
||||||
|
list: 1,
|
||||||
|
twofactor: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIcon(tf) {
|
||||||
|
switch (tf.type) {
|
||||||
|
case TFATypes.OTC:
|
||||||
|
return "Authenticator";
|
||||||
|
case TFATypes.BACKUP_CODE:
|
||||||
|
return "BackupCode";
|
||||||
|
case TFATypes.U2F:
|
||||||
|
return "SecurityKey";
|
||||||
|
case TFATypes.APP_ALLOW:
|
||||||
|
return "AppPush";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let twofactors = Api.twofactor.map(tf => {
|
||||||
|
return {
|
||||||
|
...tf,
|
||||||
|
icon: getIcon(tf)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let state = states.list;
|
||||||
|
|
||||||
|
let twofactor = undefined;
|
||||||
|
twofactor = twofactors[0];
|
||||||
|
$: console.log(twofactor);
|
||||||
|
|
||||||
|
function onFinish(res) {
|
||||||
|
if (res) finish();
|
||||||
|
else twofactor = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let finish;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
border-top: 1px grey solid;
|
||||||
|
padding: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:first-child {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
float: left;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-left: 48px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if !twofactor}
|
||||||
|
<h3>Select your Authentication method:</h3>
|
||||||
|
<ul>
|
||||||
|
{#each twofactors as tf}
|
||||||
|
<li on:click={() => (twofactor = tf)}>
|
||||||
|
<div class="icon">
|
||||||
|
<Icon icon_name={tf.icon} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name">{tf.name}</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else if twofactor.type === TFATypes.OTC}
|
||||||
|
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={true} />
|
||||||
|
{:else if twofactor.type === TFATypes.BACKUP_CODE}
|
||||||
|
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={false} />
|
||||||
|
{:else if twofactor.type === TFATypes.U2F}
|
||||||
|
<U2FTwoFactor id={twofactor.id} finish={onFinish} />
|
||||||
|
{:else if twofactor.type === TFATypes.APP_ALLOW}
|
||||||
|
<PushTwoFactor id={twofactor.id} finish={onFinish} />
|
||||||
|
{:else}
|
||||||
|
<div>Invalid TwoFactor Method!</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
182
Frontend/src/pages/login/api.ts
Normal file
182
Frontend/src/pages/login/api.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import request from "../../helper/request";
|
||||||
|
import sha from "../../helper/sha512";
|
||||||
|
import { setCookie, getCookie } from "../../helper/cookie";
|
||||||
|
|
||||||
|
export interface TwoFactor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: TFATypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TFATypes {
|
||||||
|
OTC,
|
||||||
|
BACKUP_CODE,
|
||||||
|
U2F,
|
||||||
|
APP_ALLOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
// const Api = {
|
||||||
|
// // twofactor: [{
|
||||||
|
// // id: "1",
|
||||||
|
// // name: "Backup Codes",
|
||||||
|
// // type: TFATypes.BACKUP_CODE
|
||||||
|
// // }, {
|
||||||
|
// // id: "2",
|
||||||
|
// // name: "YubiKey",
|
||||||
|
// // type: TFATypes.U2F
|
||||||
|
// // }, {
|
||||||
|
// // id: "3",
|
||||||
|
// // name: "Authenticator",
|
||||||
|
// // type: TFATypes.OTC
|
||||||
|
// // }] as TwoFactor[],
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface IToken {
|
||||||
|
token: string;
|
||||||
|
expires: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeid(length) {
|
||||||
|
var result = "";
|
||||||
|
var characters =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
var charactersLength = characters.length;
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Api {
|
||||||
|
static salt: string;
|
||||||
|
static login: IToken;
|
||||||
|
static special: IToken;
|
||||||
|
static username: string;
|
||||||
|
|
||||||
|
static twofactor: any[];
|
||||||
|
|
||||||
|
static getUsername() {
|
||||||
|
return this.username || getCookie("username");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setUsername(
|
||||||
|
username: string
|
||||||
|
): Promise<{ error: string | undefined }> {
|
||||||
|
return request(
|
||||||
|
"/api/user/login",
|
||||||
|
{
|
||||||
|
type: "username",
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
"POST"
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
this.salt = res.salt;
|
||||||
|
this.username = username;
|
||||||
|
return {
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
let error = err.message;
|
||||||
|
return { error };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setPassword(
|
||||||
|
password: string
|
||||||
|
): Promise<{ error: string | undefined; twofactor?: any }> {
|
||||||
|
const date = new Date().valueOf();
|
||||||
|
let pw = sha(sha(this.salt + password) + date.toString());
|
||||||
|
return request(
|
||||||
|
"/api/user/login",
|
||||||
|
{
|
||||||
|
type: "password",
|
||||||
|
},
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
username: this.username,
|
||||||
|
password: pw,
|
||||||
|
date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ login, special, tfa }) => {
|
||||||
|
this.login = login;
|
||||||
|
this.special = special;
|
||||||
|
|
||||||
|
if (tfa && Array.isArray(tfa) && tfa.length > 0)
|
||||||
|
this.twofactor = tfa;
|
||||||
|
else this.twofactor = undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
let error = err.message;
|
||||||
|
return { error };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static gettok() {
|
||||||
|
return {
|
||||||
|
login: this.login.token,
|
||||||
|
special: this.special.token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendBackup(id: string, code: string) {
|
||||||
|
return request("/api/user/twofactor/backup", this.gettok(), "PUT", {
|
||||||
|
code,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.then(({ login_exp, special_exp }) => {
|
||||||
|
this.login.expires = login_exp;
|
||||||
|
this.special.expires = special_exp;
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
.catch((err) => ({ error: err.message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendOTC(id: string, code: string) {
|
||||||
|
return request("/api/user/twofactor/otc", this.gettok(), "PUT", {
|
||||||
|
code,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.then(({ login_exp, special_exp }) => {
|
||||||
|
this.login.expires = login_exp;
|
||||||
|
this.special.expires = special_exp;
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
.catch((error) => ({ error: error.message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
static finish() {
|
||||||
|
let d = new Date();
|
||||||
|
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
|
||||||
|
setCookie("username", this.username, d.toUTCString());
|
||||||
|
|
||||||
|
setCookie(
|
||||||
|
"login",
|
||||||
|
this.login.token,
|
||||||
|
new Date(this.login.expires).toUTCString()
|
||||||
|
);
|
||||||
|
setCookie(
|
||||||
|
"special",
|
||||||
|
this.special.token,
|
||||||
|
new Date(this.special.expires).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;
|
||||||
|
}
|
||||||
|
setTimeout(() => (window.location.href = red), 200);
|
||||||
|
}
|
||||||
|
}
|
1
Frontend/src/pages/login/icons/AppPush.svg
Normal file
1
Frontend/src/pages/login/icons/AppPush.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M18.617,1.72c0,-0.949 -0.771,-1.72 -1.721,-1.72l-9.792,0c-0.95,0 -1.721,0.771 -1.721,1.72l0,20.56c0,0.949 0.771,1.72 1.721,1.72l9.792,0c0.95,0 1.721,-0.771 1.721,-1.72l0,-20.56Z" style="fill:#4d4d4d;"/><rect x="6" y="3" width="12" height="18" style="fill:#b3b3b3;"/><path d="M14,1.5c0,-0.129 -0.105,-0.233 -0.233,-0.233l-3.534,0c-0.128,0 -0.233,0.104 -0.233,0.233c0,0.129 0.105,0.233 0.233,0.233l3.534,0c0.128,0 0.233,-0.104 0.233,-0.233Z" style="fill:#b3b3b3;"/><ellipse cx="12" cy="22.5" rx="0.983" ry="1" style="fill:#b3b3b3;"/></svg>
|
After Width: | Height: | Size: 992 B |
1
Frontend/src/pages/login/icons/Authenticator.svg
Normal file
1
Frontend/src/pages/login/icons/Authenticator.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><path d="M18.5,12c0,3.59 -2.91,6.5 -6.5,6.5c-3.59,0 -6.5,-2.91 -6.5,-6.5c0,-3.59 2.91,-6.5 6.5,-6.5c1.729,0 3.295,0.679 4.46,1.78l4.169,-3.599c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5.5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle id="XMLID_1331_" cx="12" cy="12" r="12" style="fill:#808080;"/><path d="M19,12c0,3.866 -3.134,7 -7,7c-3.866,0 -7,-3.134 -7,-7c0,-3.866 3.134,-7 7,-7c1.88,0 3.583,0.745 4.841,1.951l3.788,-3.27c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle cx="12" cy="2.5" r="1" style="fill:#b3b3b3;"/><circle cx="12" cy="21.5" r="1" style="fill:#b3b3b3;"/><circle cx="2.5" cy="12" r="1" style="fill:#b3b3b3;"/><path d="M4.575,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M18.01,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M4.575,4.575c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><circle id="XMLID_1329_" cx="12" cy="12" r="6" style="fill:#808080;"/><circle id="XMLID_1330_" cx="12" cy="12" r="7" style="fill:#808080;"/><path d="M19,12.25c0,-0.042 -0.006,-0.083 -0.006,-0.125c-0.068,3.808 -3.17,6.875 -6.994,6.875c-3.824,0 -6.933,-3.067 -7,-6.875c-0.001,0.042 0,0.083 0,0.125c0,3.866 3.134,7 7,7c3.866,0 7,-3.134 7,-7Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M18.92,13l-3.061,0c0.083,-0.321 0.141,-0.653 0.141,-1c0,-2.209 -1.791,-4 -4,-4c-2.209,0 -4,1.791 -4,4c0,1.105 0.448,2.105 1.172,2.828c1.014,1.015 4.057,4.058 4.057,4.058c2.955,-0.525 5.263,-2.899 5.691,-5.886Z" style="fill:#4d4d4d;fill-rule:nonzero;"/><path d="M22,13l-10,0c-0.553,0 -1,-0.448 -1,-1c0,-0.552 0.447,-1 1,-1l10,0c0.553,0 1,0.448 1,1c0,0.552 -0.447,1 -1,1Z" style="fill:#b3b3b3;fill-rule:nonzero;"/><path d="M11.948,11.25l10.104,0c0.409,0 0.776,0.247 0.935,0.592c-0.08,-0.471 -0.492,-0.842 -0.987,-0.842l-10,0c-0.495,0 -0.9,0.33 -0.98,0.801c0.159,-0.345 0.519,-0.551 0.928,-0.551Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M23,12c0,0.552 -0.447,1 -1,1l-3.08,0c-0.428,2.988 -2.737,5.362 -5.693,5.886l3.935,3.946c4.04,-1.931 6.838,-6.056 6.838,-10.832l-1,0Z" style="fill:#666;fill-opacity:0.5;fill-rule:nonzero;"/><path d="M12,5c-3.866,0 -7,3.134 -7,7c0,0.042 -0.001,0.069 0,0.111c0.067,-3.808 3.176,-6.861 7,-6.861c2.828,0 4.841,1.701 4.841,1.701c-1.257,-1.198 -2.968,-1.951 -4.841,-1.951Z" style="fill-opacity:0.1;fill-rule:nonzero;"/><circle id="XMLID_4_" cx="12" cy="12" r="12" style="fill:url(#_Linear1);"/></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(21.7566,10.1453,-10.1453,21.7566,1.12171,6.92737)"><stop offset="0" style="stop-color:#fff;stop-opacity:0.2"/><stop offset="1" style="stop-color:#fff;stop-opacity:0"/></linearGradient></defs></svg>
|
After Width: | Height: | Size: 3.6 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user