Add JRPC API, reworked Login and User pages
This commit is contained in:
parent
922ed1e813
commit
e1164eb05b
@ -3,14 +3,7 @@ type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: Build with node
|
||||
image: node:12
|
||||
commands:
|
||||
- npm config set registry https://npm.hibas123.de
|
||||
- npm install
|
||||
- npm run install
|
||||
- npm run build
|
||||
- name: Publish to docker
|
||||
- name: Build docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
|
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@ -1,16 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceFolder}/lib/index.js",
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"],
|
||||
"preLaunchTask": "build"
|
||||
}
|
||||
]
|
||||
}
|
22
.vscode/tasks.json
vendored
22
.vscode/tasks.json
vendored
@ -1,22 +0,0 @@
|
||||
{
|
||||
// Unter https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// finden Sie Informationen zum Format von "tasks.json"
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build-ts",
|
||||
"group": "build",
|
||||
"problemMatcher": ["$tsc"],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "never",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"label": "build"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
[database]
|
||||
host=localhost
|
||||
database=openauth
|
||||
|
||||
[core]
|
||||
name = OpenAuthService
|
||||
|
||||
[web]
|
||||
port = 3000
|
||||
|
||||
[mail]
|
||||
server = mail.example.com
|
||||
username = test
|
||||
password = test
|
||||
port = 595
|
||||
[database]
|
||||
host=localhost
|
||||
database=openauth
|
||||
|
||||
[core]
|
||||
name = OpenAuthService
|
||||
secret = dev
|
||||
|
||||
[web]
|
||||
port = 3000
|
||||
|
||||
[mail]
|
||||
server = mail.example.com
|
||||
username = test
|
||||
password = test
|
||||
port = 595
|
||||
|
@ -39,5 +39,10 @@
|
||||
"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 (No special token)": "You are not logged in or your login is expired (No special token)",
|
||||
"Special token invalid": "Special token invalid"
|
||||
"Special token invalid": "Special token invalid",
|
||||
"You are not logged in or your login is expired (No login token)": "You are not logged in or your login is expired (No login token)",
|
||||
"": "",
|
||||
"You are not logged in or your login is expired ()": "You are not logged in or your login is expired ()",
|
||||
"Session not validated!": "Session not validated!",
|
||||
"Not logged in": "Not logged in"
|
||||
}
|
@ -1,72 +1,77 @@
|
||||
{
|
||||
"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-internalapi": "workspace:^",
|
||||
"@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"
|
||||
}
|
||||
{
|
||||
"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/express-session": "^1.17.7",
|
||||
"@types/i18n": "^0.13.6",
|
||||
"@types/ini": "^1.3.31",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/mongodb": "^4.0.7",
|
||||
"@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.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hibas123/config": "^1.1.2",
|
||||
"@hibas123/nodelogging": "^3.1.3",
|
||||
"@hibas123/nodeloggingserver_client": "^1.1.2",
|
||||
"@hibas123/openauth-internalapi": "workspace:^",
|
||||
"@hibas123/openauth-views-v1": "workspace:^",
|
||||
"@hibas123/safe_mongo": "^2.0.1",
|
||||
"@simplewebauthn/server": "^7.2.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"compression": "^1.7.4",
|
||||
"connect-mongo": "^5.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"i18n": "^0.15.1",
|
||||
"ini": "^4.0.0",
|
||||
"joi": "^17.9.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"mongodb": "^5.2.0",
|
||||
"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"
|
||||
}
|
||||
|
@ -1,111 +1,111 @@
|
||||
import { Request, Router } from "express";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import Permission from "../../models/permissions";
|
||||
import verify, { Types } from "../middlewares/verify";
|
||||
import Client from "../../models/client";
|
||||
import { ObjectID } from "bson";
|
||||
|
||||
const PermissionRoute: Router = Router();
|
||||
PermissionRoute.route("/")
|
||||
/**
|
||||
* @api {get} /admin/permission
|
||||
* @apiName AdminGetPermissions
|
||||
*
|
||||
* @apiParam client Optionally filter by client _id
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} permissions
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.name Permission name
|
||||
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
|
||||
* @apiSuccess {String} permissions.client The ID of the owning client
|
||||
*/
|
||||
.get(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let query = {};
|
||||
if (req.query.client) {
|
||||
query = { client: new ObjectID(req.query.client as string) };
|
||||
}
|
||||
let permissions = await Permission.find(query);
|
||||
res.json(permissions);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {post} /admin/permission
|
||||
* @apiName AdminAddPermission
|
||||
*
|
||||
* @apiParam client The ID of the owning client
|
||||
* @apiParam name Permission name
|
||||
* @apiParam description A description, that makes it clear to the user, what this Permission allows to do
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} permissions
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.name Permission name
|
||||
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
|
||||
* @apiSuccess {String} permissions.client The ID of the owning client
|
||||
* @apiSuccess {String} permissions.grant_type The type of the permission. "user" | "client" granted
|
||||
*/
|
||||
.post(
|
||||
verify(
|
||||
{
|
||||
client: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
name: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
description: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
type: {
|
||||
type: Types.ENUM,
|
||||
values: ["user", "client"],
|
||||
},
|
||||
},
|
||||
true
|
||||
),
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let client = await Client.findById(req.body.client);
|
||||
if (!client) {
|
||||
throw new RequestError(
|
||||
"Client not found",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
let permission = Permission.new({
|
||||
description: req.body.description,
|
||||
name: req.body.name,
|
||||
client: client._id,
|
||||
grant_type: req.body.type,
|
||||
});
|
||||
await Permission.save(permission);
|
||||
res.json(permission);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {delete} /admin/permission
|
||||
* @apiName AdminDeletePermission
|
||||
*
|
||||
* @apiParam id The permission ID
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
.delete(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
await Permission.delete(id);
|
||||
res.json({ success: true });
|
||||
})
|
||||
);
|
||||
|
||||
export default PermissionRoute;
|
||||
import { Request, Router } from "express";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import Permission from "../../models/permissions";
|
||||
import verify, { Types } from "../middlewares/verify";
|
||||
import Client from "../../models/client";
|
||||
import { ObjectId } from "bson";
|
||||
|
||||
const PermissionRoute: Router = Router();
|
||||
PermissionRoute.route("/")
|
||||
/**
|
||||
* @api {get} /admin/permission
|
||||
* @apiName AdminGetPermissions
|
||||
*
|
||||
* @apiParam client Optionally filter by client _id
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} permissions
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.name Permission name
|
||||
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
|
||||
* @apiSuccess {String} permissions.client The ID of the owning client
|
||||
*/
|
||||
.get(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let query = {};
|
||||
if (req.query.client) {
|
||||
query = { client: new ObjectId(req.query.client as string) };
|
||||
}
|
||||
let permissions = await Permission.find(query);
|
||||
res.json(permissions);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {post} /admin/permission
|
||||
* @apiName AdminAddPermission
|
||||
*
|
||||
* @apiParam client The ID of the owning client
|
||||
* @apiParam name Permission name
|
||||
* @apiParam description A description, that makes it clear to the user, what this Permission allows to do
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} permissions
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.name Permission name
|
||||
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
|
||||
* @apiSuccess {String} permissions.client The ID of the owning client
|
||||
* @apiSuccess {String} permissions.grant_type The type of the permission. "user" | "client" granted
|
||||
*/
|
||||
.post(
|
||||
verify(
|
||||
{
|
||||
client: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
name: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
description: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
type: {
|
||||
type: Types.ENUM,
|
||||
values: ["user", "client"],
|
||||
},
|
||||
},
|
||||
true
|
||||
),
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let client = await Client.findById(req.body.client);
|
||||
if (!client) {
|
||||
throw new RequestError(
|
||||
"Client not found",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
let permission = Permission.new({
|
||||
description: req.body.description,
|
||||
name: req.body.name,
|
||||
client: client._id,
|
||||
grant_type: req.body.type,
|
||||
});
|
||||
await Permission.save(permission);
|
||||
res.json(permission);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {delete} /admin/permission
|
||||
* @apiName AdminDeletePermission
|
||||
*
|
||||
* @apiParam id The permission ID
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
.delete(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
await Permission.delete(id);
|
||||
res.json({ success: true });
|
||||
})
|
||||
);
|
||||
|
||||
export default PermissionRoute;
|
||||
|
@ -1,98 +1,98 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import {
|
||||
ClientAuthMiddleware,
|
||||
GetClientAuthMiddleware,
|
||||
} from "../middlewares/client";
|
||||
import Permission from "../../models/permissions";
|
||||
import User from "../../models/user";
|
||||
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import Grant from "../../models/grants";
|
||||
import { ObjectID } from "mongodb";
|
||||
|
||||
export const GetPermissions = Stacker(
|
||||
GetClientAuthMiddleware(true),
|
||||
async (req: Request, res: Response) => {
|
||||
const { user, permission } = req.query as { [key: string]: string };
|
||||
|
||||
let permissions: { id: string; name: string; description: string }[];
|
||||
let users: string[];
|
||||
|
||||
if (user) {
|
||||
const grant = await Grant.findOne({
|
||||
client: req.client._id,
|
||||
user: new ObjectID(user),
|
||||
});
|
||||
|
||||
permissions = await Promise.all(
|
||||
grant.permissions.map((perm) => Permission.findById(perm))
|
||||
).then((res) =>
|
||||
res
|
||||
.filter((e) => e.grant_type === "client")
|
||||
.map((e) => {
|
||||
return {
|
||||
id: e._id.toHexString(),
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
const grants = await Grant.find({
|
||||
client: req.client._id,
|
||||
permissions: new ObjectID(permission),
|
||||
});
|
||||
|
||||
users = grants.map((grant) => grant.user.toHexString());
|
||||
}
|
||||
|
||||
res.json({ permissions, users });
|
||||
}
|
||||
);
|
||||
|
||||
export const PostPermissions = Stacker(
|
||||
GetClientAuthMiddleware(true),
|
||||
async (req: Request, res: Response) => {
|
||||
const { permission, uid } = req.body;
|
||||
|
||||
const user = await User.findOne({ uid });
|
||||
if (!user) {
|
||||
throw new RequestError("User not found!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const permissionDoc = await Permission.findById(permission);
|
||||
if (!permissionDoc || !permissionDoc.client.equals(req.client._id)) {
|
||||
throw new RequestError(
|
||||
"Permission not found!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let grant = await Grant.findOne({
|
||||
client: req.client._id,
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
if (!grant) {
|
||||
grant = Grant.new({
|
||||
client: req.client._id,
|
||||
user: req.user._id,
|
||||
permissions: [],
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Fix clients getting user data without consent, when a grant is created and no additional permissions are requested, since for now, it is only checked for grant existance to make client access user data
|
||||
|
||||
if (grant.permissions.indexOf(permission) < 0)
|
||||
grant.permissions.push(permission);
|
||||
|
||||
await Grant.save(grant);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import {
|
||||
ClientAuthMiddleware,
|
||||
GetClientAuthMiddleware,
|
||||
} from "../middlewares/client";
|
||||
import Permission from "../../models/permissions";
|
||||
import User from "../../models/user";
|
||||
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import Grant from "../../models/grants";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export const GetPermissions = Stacker(
|
||||
GetClientAuthMiddleware(true),
|
||||
async (req: Request, res: Response) => {
|
||||
const { user, permission } = req.query as { [key: string]: string };
|
||||
|
||||
let permissions: { id: string; name: string; description: string }[];
|
||||
let users: string[];
|
||||
|
||||
if (user) {
|
||||
const grant = await Grant.findOne({
|
||||
client: req.client._id,
|
||||
user: new ObjectId(user),
|
||||
});
|
||||
|
||||
permissions = await Promise.all(
|
||||
grant.permissions.map((perm) => Permission.findById(perm))
|
||||
).then((res) =>
|
||||
res
|
||||
.filter((e) => e.grant_type === "client")
|
||||
.map((e) => {
|
||||
return {
|
||||
id: e._id.toHexString(),
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
const grants = await Grant.find({
|
||||
client: req.client._id,
|
||||
permissions: new ObjectId(permission),
|
||||
});
|
||||
|
||||
users = grants.map((grant) => grant.user.toHexString());
|
||||
}
|
||||
|
||||
res.json({ permissions, users });
|
||||
}
|
||||
);
|
||||
|
||||
export const PostPermissions = Stacker(
|
||||
GetClientAuthMiddleware(true),
|
||||
async (req: Request, res: Response) => {
|
||||
const { permission, uid } = req.body;
|
||||
|
||||
const user = await User.findOne({ uid });
|
||||
if (!user) {
|
||||
throw new RequestError("User not found!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const permissionDoc = await Permission.findById(permission);
|
||||
if (!permissionDoc || !permissionDoc.client.equals(req.client._id)) {
|
||||
throw new RequestError(
|
||||
"Permission not found!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let grant = await Grant.findOne({
|
||||
client: req.client._id,
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
if (!grant) {
|
||||
grant = Grant.new({
|
||||
client: req.client._id,
|
||||
user: req.user._id,
|
||||
permissions: [],
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Fix clients getting user data without consent, when a grant is created and no additional permissions are requested, since for now, it is only checked for grant existance to make client access user data
|
||||
|
||||
if (grant.permissions.indexOf(permission) < 0)
|
||||
grant.permissions.push(permission);
|
||||
|
||||
await Grant.save(grant);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -1,54 +1,50 @@
|
||||
import * as express from "express";
|
||||
import AdminRoute from "./admin";
|
||||
import UserRoute from "./user";
|
||||
import InternalRoute from "./internal";
|
||||
import Login from "./user/login";
|
||||
import ClientRouter from "./client";
|
||||
import * as cors from "cors";
|
||||
import OAuthRoute from "./oauth";
|
||||
import config from "../config";
|
||||
import JRPCEndpoint from "./jrpc";
|
||||
|
||||
const ApiRouter: express.IRouter = express.Router();
|
||||
ApiRouter.use("/admin", AdminRoute);
|
||||
ApiRouter.use(cors());
|
||||
ApiRouter.use("/user", UserRoute);
|
||||
ApiRouter.use("/internal", InternalRoute);
|
||||
ApiRouter.use("/oauth", OAuthRoute);
|
||||
|
||||
ApiRouter.use("/client", ClientRouter);
|
||||
|
||||
/**
|
||||
* @api {post} /jrpc
|
||||
* @apiName InternalJRPCEndpoint
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiErrorExample {Object} Error-Response:
|
||||
{
|
||||
error: [
|
||||
{
|
||||
message: "Some Error",
|
||||
field: "username"
|
||||
}
|
||||
],
|
||||
status: 400
|
||||
}
|
||||
*/
|
||||
ApiRouter.post("/jrpc", JRPCEndpoint);
|
||||
|
||||
// Legacy reasons (deprecated)
|
||||
ApiRouter.use("/", ClientRouter);
|
||||
|
||||
// Legacy reasons (deprecated)
|
||||
ApiRouter.post("/login", Login);
|
||||
|
||||
ApiRouter.get("/config.json", (req, res) => {
|
||||
return res.json({
|
||||
name: config.core.name,
|
||||
url: config.core.url,
|
||||
});
|
||||
});
|
||||
|
||||
export default ApiRouter;
|
||||
import * as express from "express";
|
||||
import AdminRoute from "./admin";
|
||||
import UserRoute from "./user";
|
||||
import InternalRoute from "./internal";
|
||||
import ClientRouter from "./client";
|
||||
import cors from "cors";
|
||||
import OAuthRoute from "./oauth";
|
||||
import config from "../config";
|
||||
import JRPCEndpoint from "./jrpc";
|
||||
|
||||
const ApiRouter: express.IRouter = express.Router();
|
||||
ApiRouter.use("/admin", AdminRoute);
|
||||
ApiRouter.use(cors());
|
||||
ApiRouter.use("/user", UserRoute);
|
||||
ApiRouter.use("/internal", InternalRoute);
|
||||
ApiRouter.use("/oauth", OAuthRoute);
|
||||
|
||||
ApiRouter.use("/client", ClientRouter);
|
||||
|
||||
/**
|
||||
* @api {post} /jrpc
|
||||
* @apiName InternalJRPCEndpoint
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiErrorExample {Object} Error-Response:
|
||||
{
|
||||
error: [
|
||||
{
|
||||
message: "Some Error",
|
||||
field: "username"
|
||||
}
|
||||
],
|
||||
status: 400
|
||||
}
|
||||
*/
|
||||
ApiRouter.post("/jrpc", JRPCEndpoint);
|
||||
|
||||
// Legacy reasons (deprecated)
|
||||
ApiRouter.use("/", ClientRouter);
|
||||
|
||||
ApiRouter.get("/config.json", (req, res) => {
|
||||
return res.json({
|
||||
name: config.core.name,
|
||||
url: config.core.url,
|
||||
});
|
||||
});
|
||||
|
||||
export default ApiRouter;
|
||||
|
@ -1,41 +1,39 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetClientAuthMiddleware } from "../middlewares/client";
|
||||
import { UserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import moment = require("moment");
|
||||
import { randomBytes } from "crypto";
|
||||
export const OAuthInternalApp = Stacker(
|
||||
GetClientAuthMiddleware(false, true),
|
||||
UserMiddleware,
|
||||
async (req: Request, res: Response) => {
|
||||
let { redirect_uri, state } = req.query as { [key: string]: string };
|
||||
if (!redirect_uri) {
|
||||
throw new RequestError(
|
||||
"No redirect url set!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&";
|
||||
|
||||
let code = ClientCode.new({
|
||||
user: req.user._id,
|
||||
client: req.client._id,
|
||||
validTill: moment().add(30, "minutes").toDate(),
|
||||
code: randomBytes(16).toString("hex"),
|
||||
permissions: [],
|
||||
});
|
||||
await ClientCode.save(code);
|
||||
|
||||
res.redirect(
|
||||
redirect_uri +
|
||||
sep +
|
||||
"code=" +
|
||||
code.code +
|
||||
(state ? "&state=" + state : "")
|
||||
);
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetClientAuthMiddleware } from "../middlewares/client";
|
||||
import { UserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import moment = require("moment");
|
||||
import { randomBytes } from "crypto";
|
||||
export const OAuthInternalApp = Stacker(
|
||||
GetClientAuthMiddleware(false, true),
|
||||
UserMiddleware,
|
||||
async (req: Request, res: Response) => {
|
||||
let { redirect_uri, state } = req.query as { [key: string]: string };
|
||||
if (!redirect_uri) {
|
||||
throw new RequestError(
|
||||
"No redirect url set!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let redurl = new URL(redirect_uri);
|
||||
|
||||
let code = ClientCode.new({
|
||||
user: req.user._id,
|
||||
client: req.client._id,
|
||||
validTill: moment().add(30, "minutes").toDate(),
|
||||
code: randomBytes(16).toString("hex"),
|
||||
permissions: [],
|
||||
});
|
||||
await ClientCode.save(code);
|
||||
|
||||
redurl.searchParams.set("code", code.code);
|
||||
if (state)
|
||||
redurl.searchParams.set("state", state);
|
||||
|
||||
res.redirect(redurl.href);
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
|
@ -1,45 +1,38 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { IUser } from "../../models/user";
|
||||
import { Server } from "@hibas123/openauth-internalapi";
|
||||
import AccountService from "./account_service";
|
||||
import SecurityService from "./security_service";
|
||||
import { ILoginToken } from "../../models/login_token";
|
||||
|
||||
export interface SessionContext {
|
||||
user: IUser,
|
||||
request: Request,
|
||||
isAdmin: boolean,
|
||||
special: boolean,
|
||||
token: {
|
||||
login: ILoginToken,
|
||||
special?: ILoginToken,
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new Server.ServiceProvider<SessionContext>();
|
||||
provider.addService(new AccountService());
|
||||
provider.addService(new SecurityService());
|
||||
|
||||
const JRPCEndpoint = Stacker(
|
||||
GetUserMiddleware(true, true),
|
||||
async (req: Request, res: Response) => {
|
||||
const session = provider.getSession((data) => {
|
||||
res.json(data);
|
||||
}, {
|
||||
user: req.user,
|
||||
request: req,
|
||||
isAdmin: req.isAdmin,
|
||||
special: req.special,
|
||||
token: {
|
||||
login: req.token.login,
|
||||
special: req.token.special,
|
||||
}
|
||||
});
|
||||
|
||||
session.onMessage(req.body);
|
||||
}
|
||||
);
|
||||
|
||||
export default JRPCEndpoint;
|
||||
import { Format } from "@hibas123/logging";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import { Server, } from "@hibas123/openauth-internalapi";
|
||||
import { RequestObject, ResponseObject } from "@hibas123/openauth-internalapi/lib/service_base";
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import AccountService from "./services/account";
|
||||
import LoginService from "./services/login";
|
||||
import SecurityService from "./services/security";
|
||||
import TFAService from "./services/twofactor";
|
||||
|
||||
export type SessionContext = Request;
|
||||
|
||||
const provider = new Server.ServiceProvider<SessionContext>();
|
||||
provider.addService(new AccountService());
|
||||
provider.addService(new SecurityService());
|
||||
provider.addService(new TFAService());
|
||||
provider.addService(new LoginService());
|
||||
|
||||
const JRPCEndpoint = Stacker(
|
||||
async (req: Request, res: Response) => {
|
||||
let jrpcreq = req.body as RequestObject;
|
||||
let startTime = process.hrtime.bigint();
|
||||
const session = provider.getSession((data: ResponseObject) => {
|
||||
let time = process.hrtime.bigint() - startTime;
|
||||
let state = data.error ? Format.red(`err(${data.error.message})`) : Format.green("OK");
|
||||
|
||||
Logging.getChild("JRPC").log(jrpcreq.method, state, "-", (Number(time / 10000n) / 100) + "ms");
|
||||
|
||||
res.json(data);
|
||||
}, req);
|
||||
|
||||
|
||||
session.onMessage(jrpcreq);
|
||||
}
|
||||
);
|
||||
|
||||
export default JRPCEndpoint;
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { Server, Token, TwoFactor, UserRegisterInfo } from "@hibas123/openauth-internalapi";
|
||||
import type { SessionContext } from "./index";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import TwoFactorModel from "../../models/twofactor";
|
||||
import moment = require("moment");
|
||||
|
||||
export default class SecurityService extends Server.SecurityService<SessionContext> {
|
||||
async GetTokens(ctx: SessionContext): Promise<Token[]> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
let raw_token = await LoginToken.find({
|
||||
user: ctx.user._id,
|
||||
valid: true,
|
||||
});
|
||||
let token = await Promise.all(
|
||||
raw_token
|
||||
.map<Promise<Token>>(async (token) => {
|
||||
await CheckToken(token);
|
||||
return {
|
||||
id: token._id.toString(),
|
||||
special: token.special,
|
||||
ip: token.ip,
|
||||
browser: token.browser,
|
||||
isthis: token._id.equals(
|
||||
token.special ? ctx.token.special._id : ctx.token.login._id
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((t) => t !== undefined)
|
||||
);
|
||||
|
||||
return token
|
||||
}
|
||||
async RevokeToken(id: string, ctx: SessionContext): Promise<void> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
let token = await LoginToken.findById(id);
|
||||
if (!token || !token.user.equals(ctx.user._id))
|
||||
throw new Error("Invalid ID");
|
||||
token.valid = false;
|
||||
await LoginToken.save(token);
|
||||
}
|
||||
|
||||
async GetTwofactorOptions(ctx: SessionContext): Promise<TwoFactor[]> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
|
||||
let twofactor = await TwoFactorModel.find({ user: ctx.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 TwoFactorModel.save(e);
|
||||
})
|
||||
);
|
||||
|
||||
twofactor = twofactor.filter((e) => e.valid);
|
||||
let tfa = twofactor.map<TwoFactor>((e) => {
|
||||
return {
|
||||
id: e._id.toString(),
|
||||
name: e.name,
|
||||
tfatype: e.type as number,
|
||||
expires: e.expires?.valueOf()
|
||||
};
|
||||
});
|
||||
|
||||
return tfa;
|
||||
}
|
||||
}
|
@ -1,49 +1,52 @@
|
||||
import { Account, ContactInfo, Gender, Server, UserRegisterInfo } from "@hibas123/openauth-internalapi";
|
||||
import type { SessionContext } from "./index";
|
||||
import Mail from "../../models/mail";
|
||||
import User from "../../models/user";
|
||||
|
||||
export default class AccountService extends Server.AccountService<SessionContext> {
|
||||
|
||||
Register(regcode: string, info: UserRegisterInfo, ctx: SessionContext): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async GetProfile(ctx: SessionContext): Promise<Account> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
|
||||
return {
|
||||
id: ctx.user.uid,
|
||||
username: ctx.user.username,
|
||||
name: ctx.user.name,
|
||||
birthday: ctx.user.birthday.valueOf(),
|
||||
gender: ctx.user.gender as number as Gender,
|
||||
}
|
||||
}
|
||||
|
||||
async UpdateProfile(info: Account, ctx: SessionContext): Promise<void> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
ctx.user.name = info.name;
|
||||
ctx.user.birthday = new Date(info.birthday);
|
||||
ctx.user.gender = info.gender as number;
|
||||
|
||||
await User.save(ctx.user);
|
||||
}
|
||||
|
||||
async GetContactInfos(ctx: SessionContext): Promise<ContactInfo> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
let mails = await Promise.all(
|
||||
ctx.user.mails.map((mail) => Mail.findById(mail))
|
||||
);
|
||||
|
||||
let contact = {
|
||||
mail: mails.filter((e) => !!e),
|
||||
phone: ctx.user.phones,
|
||||
};
|
||||
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
import { Profile, ContactInfo, Gender, Server, UserRegisterInfo } from "@hibas123/openauth-internalapi";
|
||||
import type { SessionContext } from "../index";
|
||||
import Mail from "../../../models/mail";
|
||||
import User from "../../../models/user";
|
||||
import { RequireLogin } from "../../../helper/login";
|
||||
|
||||
export default class AccountService extends Server.AccountService<SessionContext> {
|
||||
Register(regcode: string, info: UserRegisterInfo, ctx: SessionContext): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async GetProfile(ctx: SessionContext): Promise<Profile> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
|
||||
return {
|
||||
id: ctx.user.uid,
|
||||
username: ctx.user.username,
|
||||
name: ctx.user.name,
|
||||
birthday: ctx.user.birthday.valueOf(),
|
||||
gender: ctx.user.gender as number as Gender,
|
||||
}
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async UpdateProfile(info: Profile, ctx: SessionContext): Promise<void> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
ctx.user.name = info.name;
|
||||
ctx.user.birthday = new Date(info.birthday);
|
||||
ctx.user.gender = info.gender as number;
|
||||
|
||||
await User.save(ctx.user);
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async GetContactInfos(ctx: SessionContext): Promise<ContactInfo> {
|
||||
if (!ctx.user) throw new Error("Not logged in");
|
||||
|
||||
let mails = await Promise.all(
|
||||
ctx.user.mails.map((mail) => Mail.findById(mail))
|
||||
);
|
||||
|
||||
let contact = {
|
||||
mail: mails.filter((e) => !!e),
|
||||
phone: ctx.user.phones,
|
||||
};
|
||||
|
||||
return contact;
|
||||
}
|
||||
}
|
265
Backend/src/api/jrpc/services/login.ts
Normal file
265
Backend/src/api/jrpc/services/login.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { Server, LoginState, TFAOption, TFAType } from "@hibas123/openauth-internalapi";
|
||||
import type { SessionContext } from "../index";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import User, { IUser } from "../../../models/user";
|
||||
import moment from "moment";
|
||||
import crypto from "node:crypto";
|
||||
import TwoFactor, { ITwoFactor, IWebAuthn } from "../../../models/twofactor";
|
||||
import speakeasy from "speakeasy";
|
||||
import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
|
||||
import config from "../../../config";
|
||||
|
||||
//FIXME: There are a lot of uneccessary database requests happening here. Since this is not a "hot" path, it should not matter to much, but it should be fixed nontheless.
|
||||
|
||||
export default class LoginService extends Server.LoginService<SessionContext> {
|
||||
private async getUser(username: string): Promise<IUser> {
|
||||
let user = await User.findOne({ username: username.toLowerCase() });
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
private async getLoginState(ctx: SessionContext): Promise<LoginState> {
|
||||
if (ctx.user && ctx.session.validated) {
|
||||
return {
|
||||
success: true
|
||||
}
|
||||
} else if (ctx.session.login_state) {
|
||||
//TODO: Check login_state expiration or so
|
||||
if (ctx.session.login_state.username) {
|
||||
let user = await this.getUser(ctx.session.login_state.username);
|
||||
if (!ctx.session.login_state.password_correct) {
|
||||
let passwordSalt = user.salt;
|
||||
return {
|
||||
success: false,
|
||||
username: ctx.session.login_state.username,
|
||||
password: false,
|
||||
passwordSalt: passwordSalt,
|
||||
}
|
||||
} else {
|
||||
let tfa = await this.getTwoFactors(await this.getUser(ctx.session.login_state.username))
|
||||
if (tfa.length <= 0) {
|
||||
ctx.session.user_id = user._id.toString();
|
||||
ctx.session.login_state = undefined;
|
||||
Logging.warn("This should have been set somewhere else!");
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
username: ctx.session.login_state.username,
|
||||
password: true,
|
||||
requireTwoFactor: tfa,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
username: undefined,
|
||||
password: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getTwoFactors(user: IUser): Promise<TFAOption[]> {
|
||||
let twofactors = await TwoFactor.find({
|
||||
user: user._id,
|
||||
valid: true
|
||||
})
|
||||
|
||||
return twofactors.map<TFAOption>(tf => {
|
||||
return {
|
||||
id: tf._id.toString(),
|
||||
name: tf.name,
|
||||
tfatype: tf.type as number,
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private async enableSession(ctx: SessionContext) {
|
||||
let user = await this.getUser(ctx.session.login_state.username);
|
||||
ctx.user = user;
|
||||
ctx.session.user_id = user._id.toString();
|
||||
ctx.session.login_state = undefined;
|
||||
ctx.session.validated = true;
|
||||
}
|
||||
|
||||
GetState(ctx: SessionContext): Promise<LoginState> {
|
||||
return this.getLoginState(ctx);
|
||||
}
|
||||
|
||||
async Start(username: string, ctx: SessionContext): Promise<LoginState> {
|
||||
let user = await this.getUser(username);
|
||||
|
||||
ctx.session.login_state = {
|
||||
username: username,
|
||||
password_correct: false,
|
||||
}
|
||||
|
||||
return this.getLoginState(ctx);
|
||||
}
|
||||
|
||||
async UsePassword(password_hash: string, date: number, ctx: SessionContext): Promise<LoginState> {
|
||||
if (!ctx.session.login_state) {
|
||||
throw new Error("No login state. Call Start() first.");
|
||||
}
|
||||
|
||||
let user = await this.getUser(ctx.session.login_state.username);
|
||||
|
||||
if (date <= 0) {
|
||||
if (user.password !== password_hash) {
|
||||
throw new Error("Password incorrect");
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!moment(date).isBetween(
|
||||
moment().subtract(1, "minute"),
|
||||
moment().add(1, "minute")
|
||||
)
|
||||
) {
|
||||
throw new Error("Date incorrect. Please check your devices time!");
|
||||
} else {
|
||||
let upw = crypto
|
||||
.createHash("sha512")
|
||||
.update(user.password + date.toString())
|
||||
.digest("hex");
|
||||
|
||||
if (upw !== password_hash) {
|
||||
throw new Error("Password incorrect");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.session.login_state.password_correct = true;
|
||||
|
||||
let tfas = await this.getTwoFactors(user);
|
||||
if (tfas.length <= 0) {
|
||||
await this.enableSession(ctx);
|
||||
}
|
||||
|
||||
return this.getLoginState(ctx);
|
||||
}
|
||||
|
||||
|
||||
private async getAndCheckTFA<T extends ITwoFactor>(id: string, shouldType: TFAType, ctx: SessionContext): Promise<T> {
|
||||
if (!ctx.session.login_state) {
|
||||
throw new Error("No login state. Call Start() first.");
|
||||
}
|
||||
|
||||
let user = await this.getUser(ctx.session.login_state.username);
|
||||
|
||||
let tfa = await TwoFactor.findById(id);
|
||||
if (!tfa || tfa.user.toString() != user._id.toString()) {
|
||||
throw new Error("Two factor not found");
|
||||
}
|
||||
|
||||
if (tfa.type != shouldType as number) {
|
||||
throw new Error("Two factor is not the correct type!");
|
||||
}
|
||||
|
||||
if (!tfa.valid) {
|
||||
throw new Error("Two factor is not valid");
|
||||
}
|
||||
|
||||
if (tfa.expires && moment().isAfter(moment(tfa.expires))) {
|
||||
throw new Error("Two factor is expired");
|
||||
}
|
||||
|
||||
return tfa as T;
|
||||
}
|
||||
|
||||
async UseTOTP(id: string, code: string, ctx: SessionContext): Promise<LoginState> {
|
||||
let tfa = await this.getAndCheckTFA(id, TFAType.TOTP, ctx);
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: tfa.data,
|
||||
encoding: "base32",
|
||||
token: code,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new Error("Code incorrect");
|
||||
}
|
||||
|
||||
await this.enableSession(ctx);
|
||||
|
||||
return this.getLoginState(ctx);
|
||||
}
|
||||
async UseBackupCode(id: string, code: string, ctx: SessionContext): Promise<LoginState> {
|
||||
let tfa = await this.getAndCheckTFA(id, TFAType.BACKUP_CODE, ctx);
|
||||
|
||||
if (tfa.data.indexOf(code) < 0) {
|
||||
throw new Error("Code incorrect");
|
||||
}
|
||||
|
||||
tfa.data = tfa.data.filter(c => c != code);
|
||||
|
||||
await TwoFactor.save(tfa);
|
||||
//TODO: handle the case where the last backup code is used
|
||||
|
||||
await this.enableSession(ctx);
|
||||
|
||||
return this.getLoginState(ctx);
|
||||
}
|
||||
|
||||
async GetWebAuthnChallenge(id: string, ctx: SessionContext): Promise<string> {
|
||||
let tfa = await this.getAndCheckTFA<IWebAuthn>(id, TFAType.WEBAUTHN, ctx);
|
||||
|
||||
const rpID = new URL(config.core.url).hostname;
|
||||
|
||||
let options = generateAuthenticationOptions({
|
||||
timeout: 60000,
|
||||
userVerification: "discouraged",
|
||||
rpID,
|
||||
allowCredentials: [{
|
||||
id: tfa.data.device.credentialID.buffer,
|
||||
type: "public-key",
|
||||
transports: tfa.data.device.transports
|
||||
}]
|
||||
})
|
||||
|
||||
ctx.session.login_state.webauthn_challenge = options.challenge;
|
||||
|
||||
Logging.debug("Challenge", options, tfa, tfa.data.device.credentialID);
|
||||
|
||||
return JSON.stringify(options);
|
||||
}
|
||||
|
||||
async UseWebAuthn(id: string, response: string, ctx: SessionContext): Promise<LoginState> {
|
||||
let tfa = await this.getAndCheckTFA<IWebAuthn>(id, TFAType.WEBAUTHN, ctx);
|
||||
|
||||
if (!ctx.session.login_state.webauthn_challenge) {
|
||||
throw new Error("No webauthn challenge");
|
||||
}
|
||||
|
||||
let rpID = new URL(config.core.url).hostname;
|
||||
|
||||
let verification = await verifyAuthenticationResponse({
|
||||
response: JSON.parse(response),
|
||||
authenticator: {
|
||||
counter: tfa.data.device.counter,
|
||||
credentialID: tfa.data.device.credentialID.buffer,
|
||||
credentialPublicKey: tfa.data.device.credentialPublicKey.buffer,
|
||||
transports: tfa.data.device.transports
|
||||
},
|
||||
expectedChallenge: ctx.session.login_state.webauthn_challenge,
|
||||
expectedOrigin: config.core.url,
|
||||
expectedRPID: rpID,
|
||||
requireUserVerification: false
|
||||
})
|
||||
|
||||
if (verification.verified) {
|
||||
tfa.data.device.counter = verification.authenticationInfo.newCounter;
|
||||
await TwoFactor.save(tfa);
|
||||
}
|
||||
|
||||
await this.enableSession(ctx);
|
||||
|
||||
return this.getLoginState(ctx);
|
||||
}
|
||||
}
|
35
Backend/src/api/jrpc/services/security.ts
Normal file
35
Backend/src/api/jrpc/services/security.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Server, Session } from "@hibas123/openauth-internalapi";
|
||||
import type { SessionContext } from "../index";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import { RequireLogin } from "../../../helper/login";
|
||||
import crypto from "node:crypto";
|
||||
import User from "../../../models/user";
|
||||
|
||||
export default class SecurityService extends Server.SecurityService<SessionContext> {
|
||||
@RequireLogin()
|
||||
async GetSessions(ctx: SessionContext): Promise<Session[]> {
|
||||
return []
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
@RequireLogin()
|
||||
async RevokeSession(id: string, ctx: SessionContext): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async ChangePassword(old_pw: string, new_pw: string, ctx: SessionContext): Promise<void> {
|
||||
let old_pw_hash = crypto.createHash("sha512").update(ctx.user.salt + old_pw).digest("hex");
|
||||
|
||||
if (old_pw_hash != ctx.user.password) {
|
||||
throw new Error("Wrong password");
|
||||
}
|
||||
|
||||
let salt = crypto.randomBytes(32).toString("base64");
|
||||
let password_hash = crypto.createHash("sha512").update(salt + new_pw).digest("hex");
|
||||
|
||||
ctx.user.salt = salt;
|
||||
ctx.user.password = password_hash;
|
||||
|
||||
await User.save(ctx.user);
|
||||
}
|
||||
}
|
194
Backend/src/api/jrpc/services/twofactor.ts
Normal file
194
Backend/src/api/jrpc/services/twofactor.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import { TFANewTOTP, Server, TFAOption, UserRegisterInfo, TFAWebAuthRegister } from "@hibas123/openauth-internalapi";
|
||||
import type { SessionContext } from "../index";
|
||||
import TwoFactorModel, { ITOTP, IWebAuthn, TFATypes } from "../../../models/twofactor";
|
||||
import moment = require("moment");
|
||||
import * as speakeasy from "speakeasy";
|
||||
import * as qrcode from "qrcode";
|
||||
import config from "../../../config";
|
||||
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
|
||||
import type { RegistrationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import { Binary } from "mongodb";
|
||||
import { RequireLogin } from "../../../helper/login";
|
||||
|
||||
|
||||
export default class TFAService extends Server.TFAService<SessionContext> {
|
||||
@RequireLogin()
|
||||
AddBackupCodes(name: string, ctx: SessionContext): Promise<string[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
RemoveBackupCodes(id: string, ctx: SessionContext): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async GetOptions(ctx: SessionContext): Promise<TFAOption[]> {
|
||||
let twofactor = await TwoFactorModel.find({ user: ctx.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 TwoFactorModel.save(e);
|
||||
})
|
||||
);
|
||||
|
||||
twofactor = twofactor.filter((e) => e.valid);
|
||||
let tfa = twofactor.map<TFAOption>((e) => {
|
||||
return {
|
||||
id: e._id.toString(),
|
||||
name: e.name,
|
||||
tfatype: e.type as number,
|
||||
expires: e.expires?.valueOf()
|
||||
};
|
||||
});
|
||||
|
||||
return tfa;
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async Delete(id: string, ctx: SessionContext): Promise<void> {
|
||||
let twofactor = await TwoFactorModel.findById(id);
|
||||
if (!twofactor || !twofactor.user.equals(ctx.user._id))
|
||||
throw new Error("Invalid ID");
|
||||
|
||||
twofactor.valid = false;
|
||||
await TwoFactorModel.save(twofactor);
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async AddTOTP(name: string, ctx: SessionContext): Promise<TFANewTOTP> {
|
||||
//Generating new
|
||||
let secret = speakeasy.generateSecret({
|
||||
name: config.core.name,
|
||||
issuer: config.core.name,
|
||||
otpauth_url: true
|
||||
});
|
||||
|
||||
let twofactor = TwoFactorModel.new(<ITOTP>{
|
||||
name: name,
|
||||
user: ctx.user._id,
|
||||
type: TFATypes.TOTP,
|
||||
valid: false,
|
||||
data: secret.base32,
|
||||
});
|
||||
|
||||
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
||||
await TwoFactorModel.save(twofactor);
|
||||
|
||||
return {
|
||||
id: twofactor._id.toString(),
|
||||
qr: dataurl,
|
||||
secret: secret.base32
|
||||
}
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async VerifyTOTP(id: string, code: string, ctx: SessionContext): Promise<void> {
|
||||
let twofactor = await TwoFactorModel.findById(id);
|
||||
if (!twofactor || !twofactor.user.equals(ctx.user._id))
|
||||
throw new Error("Invalid ID");
|
||||
|
||||
let verified = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code,
|
||||
});
|
||||
|
||||
if (!verified) throw new Error("Invalid code");
|
||||
|
||||
twofactor.valid = true;
|
||||
twofactor.expires = undefined;
|
||||
|
||||
await TwoFactorModel.save(twofactor);
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async AddWebauthn(name: string, ctx: SessionContext): Promise<TFAWebAuthRegister> {
|
||||
// TODO: Get already registered options
|
||||
|
||||
const rpID = new URL(config.core.url).hostname;
|
||||
const options = generateRegistrationOptions({
|
||||
rpName: config.core.name,
|
||||
rpID,
|
||||
userID: ctx.user.uid,
|
||||
userName: ctx.user.username,
|
||||
attestationType: 'direct',
|
||||
userDisplayName: ctx.user.name,
|
||||
excludeCredentials: [],
|
||||
authenticatorSelection: {
|
||||
userVerification: "required",
|
||||
requireResidentKey: false,
|
||||
residentKey: "discouraged",
|
||||
authenticatorAttachment: "cross-platform"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const twofactor = TwoFactorModel.new({
|
||||
name,
|
||||
type: TFATypes.WEBAUTHN,
|
||||
user: ctx.user._id,
|
||||
valid: false,
|
||||
data: {
|
||||
challenge: options.challenge
|
||||
}
|
||||
});
|
||||
|
||||
await TwoFactorModel.save(twofactor);
|
||||
|
||||
Logging.debug(twofactor);
|
||||
|
||||
return {
|
||||
id: twofactor._id.toString(),
|
||||
challenge: JSON.stringify(options)
|
||||
};
|
||||
}
|
||||
|
||||
@RequireLogin()
|
||||
async VerifyWebAuthn(id: string, registration: string, ctx: SessionContext): Promise<void> {
|
||||
let twofactor = await TwoFactorModel.findById(id) as IWebAuthn;
|
||||
if (!twofactor || !twofactor.user.equals(ctx.user._id))
|
||||
throw new Error("Invalid ID");
|
||||
|
||||
const rpID = new URL(config.core.url).hostname;
|
||||
|
||||
const response = JSON.parse(registration) as RegistrationResponseJSON;
|
||||
|
||||
let verification = await verifyRegistrationResponse({
|
||||
response,
|
||||
expectedChallenge: twofactor.data.challenge,
|
||||
expectedOrigin: config.core.url,
|
||||
expectedRPID: rpID,
|
||||
requireUserVerification: true,
|
||||
});
|
||||
|
||||
if (verification.verified) {
|
||||
const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;
|
||||
|
||||
//TODO: Check if already registered!
|
||||
// TwoFactorModel.find({
|
||||
// data: {
|
||||
// }
|
||||
// })
|
||||
|
||||
twofactor.data = {
|
||||
device: {
|
||||
credentialPublicKey: new Binary(credentialPublicKey),
|
||||
credentialID: new Binary(credentialID),
|
||||
counter: verification.registrationInfo.counter,
|
||||
transports: response.response.transports as any[]
|
||||
}
|
||||
}
|
||||
|
||||
twofactor.valid = true;
|
||||
|
||||
await TwoFactorModel.save(twofactor);
|
||||
} else {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,106 +1,70 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import User from "../../models/user";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
|
||||
class Invalid extends Error {}
|
||||
|
||||
/**
|
||||
* Returns customized Middleware function, that could also be called directly
|
||||
* by code and will return true or false depending on the token. In the false
|
||||
* case it will also send error and redirect if json is not set
|
||||
* @param json 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 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
|
||||
*/
|
||||
export function GetUserMiddleware(
|
||||
json = false,
|
||||
special_required: boolean = false,
|
||||
redirect_uri?: string,
|
||||
validated = true
|
||||
) {
|
||||
return promiseMiddleware(async function (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next?: NextFunction
|
||||
) {
|
||||
const invalid = (message: string) => {
|
||||
throw new Invalid(req.__(message));
|
||||
};
|
||||
try {
|
||||
let { login, special } = req.query as { [key: string]: string };
|
||||
if (!login) {
|
||||
login = req.cookies.login;
|
||||
special = req.cookies.special;
|
||||
}
|
||||
if (!login) invalid("No login token");
|
||||
if (!special && special_required) invalid("No special token");
|
||||
|
||||
let token = await LoginToken.findOne({ token: login, valid: true });
|
||||
if (!(await CheckToken(token, validated)))
|
||||
invalid("Login token invalid");
|
||||
|
||||
let user = await User.findById(token.user);
|
||||
if (!user) {
|
||||
token.valid = false;
|
||||
await LoginToken.save(token);
|
||||
invalid("Login token invalid");
|
||||
}
|
||||
|
||||
let special_token;
|
||||
if (special) {
|
||||
Logging.debug("Special found");
|
||||
special_token = await LoginToken.findOne({
|
||||
token: special,
|
||||
special: true,
|
||||
valid: true,
|
||||
user: token.user,
|
||||
});
|
||||
if (!(await CheckToken(special_token, validated)))
|
||||
invalid("Special token invalid");
|
||||
req.special = true;
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
req.isAdmin = user.admin;
|
||||
req.token = {
|
||||
login: token,
|
||||
special: special_token,
|
||||
};
|
||||
|
||||
if (next) next();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Invalid) {
|
||||
if (req.method === "GET" && !json) {
|
||||
res.status(HttpStatusCode.UNAUTHORIZED);
|
||||
res.redirect(
|
||||
"/login?base64=true&state=" +
|
||||
Buffer.from(
|
||||
redirect_uri ? redirect_uri : req.originalUrl
|
||||
).toString("base64")
|
||||
);
|
||||
} else {
|
||||
throw new RequestError(
|
||||
req.__(
|
||||
"You are not logged in or your login is expired" +
|
||||
` (${e.message})`
|
||||
),
|
||||
HttpStatusCode.UNAUTHORIZED,
|
||||
undefined,
|
||||
{ auth: true }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (next) next(e);
|
||||
else throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const UserMiddleware = GetUserMiddleware();
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import { requireLoginState } from "../../helper/login";
|
||||
|
||||
class Invalid extends Error { }
|
||||
|
||||
/**
|
||||
* Returns customized Middleware function, that could also be called directly
|
||||
* by code and will return true or false depending on the token. In the false
|
||||
* case it will also send error and redirect if json is not set
|
||||
* @param json 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 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
|
||||
*/
|
||||
export function GetUserMiddleware(
|
||||
json = false,
|
||||
special_required: boolean = false,
|
||||
redirect_uri?: string,
|
||||
validated = true
|
||||
) {
|
||||
return promiseMiddleware(async function (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next?: NextFunction
|
||||
) {
|
||||
const invalid = (message: string) => {
|
||||
throw new Invalid(req.__(message));
|
||||
};
|
||||
try {
|
||||
if (!requireLoginState(req, validated, special_required)) {
|
||||
invalid("Not logged in");
|
||||
}
|
||||
|
||||
if (next) next();
|
||||
return true;
|
||||
} catch (e) {
|
||||
Logging.getChild("UserMiddleware").warn(e);
|
||||
if (e instanceof Invalid) {
|
||||
if (req.method === "GET" && !json) {
|
||||
res.status(HttpStatusCode.UNAUTHORIZED);
|
||||
res.redirect(
|
||||
"/login?base64=true&state=" +
|
||||
Buffer.from(
|
||||
redirect_uri ? redirect_uri : req.originalUrl
|
||||
).toString("base64")
|
||||
);
|
||||
} else {
|
||||
throw new RequestError(
|
||||
req.__(
|
||||
"You are not logged in or your login is expired" +
|
||||
` (${e.message})`
|
||||
),
|
||||
HttpStatusCode.UNAUTHORIZED,
|
||||
undefined,
|
||||
{ auth: true }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (next) next(e);
|
||||
else throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const UserMiddleware = GetUserMiddleware();
|
||||
|
@ -1,249 +1,249 @@
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { Request, Response } from "express";
|
||||
import Client from "../../models/client";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import Permission, { IPermission } from "../../models/permissions";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import moment = require("moment");
|
||||
import { randomBytes } from "crypto";
|
||||
// import { ObjectID } from "bson";
|
||||
import Grant, { IGrant } from "../../models/grants";
|
||||
import GetAuthPage from "../../views/authorize";
|
||||
import { ObjectID } from "mongodb";
|
||||
|
||||
// const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => {
|
||||
// let { response_type, client_id, redirect_uri, scope, state, nored } = req.query;
|
||||
// const sendError = (type) => {
|
||||
// if (redirect_uri === "$local")
|
||||
// redirect_uri = "/code";
|
||||
// res.redirect(redirect_uri += `?error=${type}&state=${state}`);
|
||||
// }
|
||||
// /**
|
||||
// * error
|
||||
// REQUIRED. A single ASCII [USASCII] error code from the
|
||||
// following:
|
||||
// invalid_request
|
||||
// The request is missing a required parameter, includes an
|
||||
// invalid parameter value, includes a parameter more than
|
||||
// once, or is otherwise malformed.
|
||||
// unauthorized_client
|
||||
// The client is not authorized to request an authorization
|
||||
// code using this method.
|
||||
// access_denied
|
||||
// The resource owner or authorization server denied the
|
||||
// request.
|
||||
// */
|
||||
// try {
|
||||
|
||||
// if (response_type !== "code") {
|
||||
// return sendError("unsupported_response_type");
|
||||
// } else {
|
||||
// let client = await Client.findOne({ client_id: client_id })
|
||||
// if (!client) {
|
||||
// return sendError("unauthorized_client")
|
||||
// }
|
||||
|
||||
// if (redirect_uri && client.redirect_url !== redirect_uri) {
|
||||
// Logging.log(redirect_uri, client.redirect_url);
|
||||
// return res.send("Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!");
|
||||
// }
|
||||
|
||||
// let permissions: IPermission[] = [];
|
||||
// if (scope) {
|
||||
// let perms = (<string>scope).split(";").filter(e => e !== "read_user").map(p => new ObjectID(p));
|
||||
// permissions = await Permission.find({ _id: { $in: perms } })
|
||||
|
||||
// if (permissions.length != perms.length) {
|
||||
// return sendError("invalid_scope");
|
||||
// }
|
||||
// }
|
||||
|
||||
// let code = ClientCode.new({
|
||||
// user: req.user._id,
|
||||
// client: client._id,
|
||||
// permissions: permissions.map(p => p._id),
|
||||
// validTill: moment().add(30, "minutes").toDate(),
|
||||
// code: randomBytes(16).toString("hex")
|
||||
// });
|
||||
// await ClientCode.save(code);
|
||||
|
||||
// let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||
|
||||
// let ruri = redir + `?code=${code.code}&state=${state}`;
|
||||
// if (nored === "true") {
|
||||
// res.json({
|
||||
// redirect_uri: ruri
|
||||
// })
|
||||
// } else {
|
||||
// res.redirect(ruri);
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// Logging.error(err);
|
||||
// sendError("server_error")
|
||||
// }
|
||||
// })
|
||||
|
||||
const GetAuthRoute = (view = false) =>
|
||||
Stacker(GetUserMiddleware(false), async (req: Request, res: Response) => {
|
||||
let {
|
||||
response_type,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope = "",
|
||||
state,
|
||||
nored,
|
||||
} = req.query as { [key: string]: string };
|
||||
const sendError = (type) => {
|
||||
if (redirect_uri === "$local") redirect_uri = "/code";
|
||||
res.redirect(
|
||||
(redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)
|
||||
);
|
||||
};
|
||||
|
||||
const scopes = scope.split(";").filter((e: string) => e !== "");
|
||||
|
||||
Logging.debug("Scopes:", scope);
|
||||
|
||||
try {
|
||||
if (response_type !== "code") {
|
||||
return sendError("unsupported_response_type");
|
||||
} else {
|
||||
let client = await Client.findOne({ client_id: client_id });
|
||||
if (!client) {
|
||||
return sendError("unauthorized_client");
|
||||
}
|
||||
|
||||
if (redirect_uri && client.redirect_url !== redirect_uri) {
|
||||
Logging.log(redirect_uri, client.redirect_url);
|
||||
return res.send(
|
||||
"Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!"
|
||||
);
|
||||
}
|
||||
|
||||
let permissions: IPermission[] = [];
|
||||
let proms: PromiseLike<void>[] = [];
|
||||
if (scopes) {
|
||||
for (let perm of scopes.filter((e) => e !== "read_user")) {
|
||||
let oid = undefined;
|
||||
try {
|
||||
oid = new ObjectID(perm);
|
||||
} catch (err) {
|
||||
Logging.error(err);
|
||||
continue;
|
||||
}
|
||||
proms.push(
|
||||
Permission.findById(oid).then((p) => {
|
||||
if (!p) return Promise.reject(new Error());
|
||||
permissions.push(p);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let err = undefined;
|
||||
await Promise.all(proms).catch((e) => {
|
||||
err = e;
|
||||
});
|
||||
|
||||
if (err) {
|
||||
Logging.error(err);
|
||||
return sendError("invalid_scope");
|
||||
}
|
||||
|
||||
let grant: IGrant | undefined = await Grant.findOne({
|
||||
client: client._id,
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
Logging.debug("Grant", grant, permissions);
|
||||
|
||||
let missing_permissions: IPermission[] = [];
|
||||
|
||||
if (grant) {
|
||||
missing_permissions = grant.permissions
|
||||
.map((perm) => permissions.find((p) => p._id.equals(perm)))
|
||||
.filter((e) => !!e);
|
||||
} else {
|
||||
missing_permissions = permissions;
|
||||
}
|
||||
|
||||
let client_granted_perm = missing_permissions.filter(
|
||||
(e) => e.grant_type == "client"
|
||||
);
|
||||
if (client_granted_perm.length > 0) {
|
||||
return sendError("no_permission");
|
||||
}
|
||||
|
||||
if (!grant && missing_permissions.length > 0) {
|
||||
await new Promise<void>((yes, no) =>
|
||||
GetUserMiddleware(false, true)(
|
||||
req,
|
||||
res,
|
||||
(err?: Error | string) => (err ? no(err) : yes())
|
||||
)
|
||||
); // Maybe unresolved when redirect is happening
|
||||
|
||||
if (view) {
|
||||
res.send(
|
||||
GetAuthPage(
|
||||
req.__,
|
||||
client.name,
|
||||
permissions.map((perm) => {
|
||||
return {
|
||||
name: perm.name,
|
||||
description: perm.description,
|
||||
logo: client.logo,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
if ((req.body.allow = "true")) {
|
||||
if (!grant)
|
||||
grant = Grant.new({
|
||||
client: client._id,
|
||||
user: req.user._id,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
grant.permissions.push(
|
||||
...missing_permissions.map((e) => e._id)
|
||||
);
|
||||
await Grant.save(grant);
|
||||
} else {
|
||||
return sendError("access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = ClientCode.new({
|
||||
user: req.user._id,
|
||||
client: client._id,
|
||||
permissions: permissions.map((p) => p._id),
|
||||
validTill: moment().add(30, "minutes").toDate(),
|
||||
code: randomBytes(16).toString("hex"),
|
||||
});
|
||||
await ClientCode.save(code);
|
||||
|
||||
let redir =
|
||||
client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||
let ruri =
|
||||
redir + `?code=${code.code}${state ? "&state=" + state : ""}`;
|
||||
if (nored === "true") {
|
||||
res.json({
|
||||
redirect_uri: ruri,
|
||||
});
|
||||
} else {
|
||||
res.redirect(ruri);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logging.error(err);
|
||||
sendError("server_error");
|
||||
}
|
||||
});
|
||||
|
||||
export default GetAuthRoute;
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { Request, Response } from "express";
|
||||
import Client from "../../models/client";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import Permission, { IPermission } from "../../models/permissions";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import moment = require("moment");
|
||||
import { randomBytes } from "crypto";
|
||||
// import { ObjectId } from "bson";
|
||||
import Grant, { IGrant } from "../../models/grants";
|
||||
import GetAuthPage from "../../views/authorize";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
// const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => {
|
||||
// let { response_type, client_id, redirect_uri, scope, state, nored } = req.query;
|
||||
// const sendError = (type) => {
|
||||
// if (redirect_uri === "$local")
|
||||
// redirect_uri = "/code";
|
||||
// res.redirect(redirect_uri += `?error=${type}&state=${state}`);
|
||||
// }
|
||||
// /**
|
||||
// * error
|
||||
// REQUIRED. A single ASCII [USASCII] error code from the
|
||||
// following:
|
||||
// invalid_request
|
||||
// The request is missing a required parameter, includes an
|
||||
// invalid parameter value, includes a parameter more than
|
||||
// once, or is otherwise malformed.
|
||||
// unauthorized_client
|
||||
// The client is not authorized to request an authorization
|
||||
// code using this method.
|
||||
// access_denied
|
||||
// The resource owner or authorization server denied the
|
||||
// request.
|
||||
// */
|
||||
// try {
|
||||
|
||||
// if (response_type !== "code") {
|
||||
// return sendError("unsupported_response_type");
|
||||
// } else {
|
||||
// let client = await Client.findOne({ client_id: client_id })
|
||||
// if (!client) {
|
||||
// return sendError("unauthorized_client")
|
||||
// }
|
||||
|
||||
// if (redirect_uri && client.redirect_url !== redirect_uri) {
|
||||
// Logging.log(redirect_uri, client.redirect_url);
|
||||
// return res.send("Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!");
|
||||
// }
|
||||
|
||||
// let permissions: IPermission[] = [];
|
||||
// if (scope) {
|
||||
// let perms = (<string>scope).split(";").filter(e => e !== "read_user").map(p => new ObjectId(p));
|
||||
// permissions = await Permission.find({ _id: { $in: perms } })
|
||||
|
||||
// if (permissions.length != perms.length) {
|
||||
// return sendError("invalid_scope");
|
||||
// }
|
||||
// }
|
||||
|
||||
// let code = ClientCode.new({
|
||||
// user: req.user._id,
|
||||
// client: client._id,
|
||||
// permissions: permissions.map(p => p._id),
|
||||
// validTill: moment().add(30, "minutes").toDate(),
|
||||
// code: randomBytes(16).toString("hex")
|
||||
// });
|
||||
// await ClientCode.save(code);
|
||||
|
||||
// let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||
|
||||
// let ruri = redir + `?code=${code.code}&state=${state}`;
|
||||
// if (nored === "true") {
|
||||
// res.json({
|
||||
// redirect_uri: ruri
|
||||
// })
|
||||
// } else {
|
||||
// res.redirect(ruri);
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// Logging.error(err);
|
||||
// sendError("server_error")
|
||||
// }
|
||||
// })
|
||||
|
||||
const GetAuthRoute = (view = false) =>
|
||||
Stacker(GetUserMiddleware(false), async (req: Request, res: Response) => {
|
||||
let {
|
||||
response_type,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope = "",
|
||||
state,
|
||||
nored,
|
||||
} = req.query as { [key: string]: string };
|
||||
const sendError = (type) => {
|
||||
if (redirect_uri === "$local") redirect_uri = "/code";
|
||||
res.redirect(
|
||||
(redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)
|
||||
);
|
||||
};
|
||||
|
||||
const scopes = scope.split(";").filter((e: string) => e !== "");
|
||||
|
||||
Logging.debug("Scopes:", scope);
|
||||
|
||||
try {
|
||||
if (response_type !== "code") {
|
||||
return sendError("unsupported_response_type");
|
||||
} else {
|
||||
let client = await Client.findOne({ client_id: client_id });
|
||||
if (!client) {
|
||||
return sendError("unauthorized_client");
|
||||
}
|
||||
|
||||
if (redirect_uri && client.redirect_url !== redirect_uri) {
|
||||
Logging.log(redirect_uri, client.redirect_url);
|
||||
return res.send(
|
||||
"Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!"
|
||||
);
|
||||
}
|
||||
|
||||
let permissions: IPermission[] = [];
|
||||
let proms: PromiseLike<void>[] = [];
|
||||
if (scopes) {
|
||||
for (let perm of scopes.filter((e) => e !== "read_user")) {
|
||||
let oid = undefined;
|
||||
try {
|
||||
oid = new ObjectId(perm);
|
||||
} catch (err) {
|
||||
Logging.error(err);
|
||||
continue;
|
||||
}
|
||||
proms.push(
|
||||
Permission.findById(oid).then((p) => {
|
||||
if (!p) return Promise.reject(new Error());
|
||||
permissions.push(p);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let err = undefined;
|
||||
await Promise.all(proms).catch((e) => {
|
||||
err = e;
|
||||
});
|
||||
|
||||
if (err) {
|
||||
Logging.error(err);
|
||||
return sendError("invalid_scope");
|
||||
}
|
||||
|
||||
let grant: IGrant | undefined = await Grant.findOne({
|
||||
client: client._id,
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
Logging.debug("Grant", grant, permissions);
|
||||
|
||||
let missing_permissions: IPermission[] = [];
|
||||
|
||||
if (grant) {
|
||||
missing_permissions = grant.permissions
|
||||
.map((perm) => permissions.find((p) => p._id.equals(perm)))
|
||||
.filter((e) => !!e);
|
||||
} else {
|
||||
missing_permissions = permissions;
|
||||
}
|
||||
|
||||
let client_granted_perm = missing_permissions.filter(
|
||||
(e) => e.grant_type == "client"
|
||||
);
|
||||
if (client_granted_perm.length > 0) {
|
||||
return sendError("no_permission");
|
||||
}
|
||||
|
||||
if (!grant && missing_permissions.length > 0) {
|
||||
await new Promise<void>((yes, no) =>
|
||||
GetUserMiddleware(false, true)(
|
||||
req,
|
||||
res,
|
||||
(err?: Error | string) => (err ? no(err) : yes())
|
||||
)
|
||||
); // Maybe unresolved when redirect is happening
|
||||
|
||||
if (view) {
|
||||
res.send(
|
||||
GetAuthPage(
|
||||
req.__,
|
||||
client.name,
|
||||
permissions.map((perm) => {
|
||||
return {
|
||||
name: perm.name,
|
||||
description: perm.description,
|
||||
logo: client.logo,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
if ((req.body.allow = "true")) {
|
||||
if (!grant)
|
||||
grant = Grant.new({
|
||||
client: client._id,
|
||||
user: req.user._id,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
grant.permissions.push(
|
||||
...missing_permissions.map((e) => e._id)
|
||||
);
|
||||
await Grant.save(grant);
|
||||
} else {
|
||||
return sendError("access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = ClientCode.new({
|
||||
user: req.user._id,
|
||||
client: client._id,
|
||||
permissions: permissions.map((p) => p._id),
|
||||
validTill: moment().add(30, "minutes").toDate(),
|
||||
code: randomBytes(16).toString("hex"),
|
||||
});
|
||||
await ClientCode.save(code);
|
||||
|
||||
let redir =
|
||||
client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||
let ruri =
|
||||
redir + `?code=${code.code}${state ? "&state=" + state : ""}`;
|
||||
if (nored === "true") {
|
||||
res.json({
|
||||
redirect_uri: ruri,
|
||||
});
|
||||
} else {
|
||||
res.redirect(ruri);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logging.error(err);
|
||||
sendError("server_error");
|
||||
}
|
||||
});
|
||||
|
||||
export default GetAuthRoute;
|
||||
|
@ -1,19 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
);
|
@ -1,19 +0,0 @@
|
||||
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,132 +1,39 @@
|
||||
import { Router } from "express";
|
||||
import { GetAccount } from "./account";
|
||||
import { GetContactInfos } from "./contact";
|
||||
import Login from "./login";
|
||||
import Register from "./register";
|
||||
import { DeleteToken, GetToken } from "./token";
|
||||
import TwoFactorRoute from "./twofactor";
|
||||
import OAuthRoute from "./oauth";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
|
||||
/**
|
||||
* @api {post} /user/register
|
||||
* @apiName UserRegister
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiParam {String} mail EMail linked to this Account
|
||||
* @apiParam {String} username The new Username
|
||||
* @apiParam {String} password Password hashed and salted like specification
|
||||
* @apiParam {String} salt The Salt used for password hashing
|
||||
* @apiParam {String} regcode The regcode, that should be used
|
||||
* @apiParam {String} gender Gender can be: "male", "female", "other", "none"
|
||||
* @apiParam {String} name The real name of the User
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*
|
||||
* @apiErrorExample {Object} Error-Response:
|
||||
{
|
||||
error: [
|
||||
{
|
||||
message: "Some Error",
|
||||
field: "username"
|
||||
}
|
||||
],
|
||||
status: 400
|
||||
}
|
||||
*/
|
||||
UserRoute.post("/register", Register);
|
||||
|
||||
/**
|
||||
* @api {post} /user/login?type=:type
|
||||
* @apiName UserLogin
|
||||
*
|
||||
* @apiParam {String} type Type could be either "username" or "password"
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiParam {String} username Username (either username or uid required)
|
||||
* @apiParam {String} uid (either username or uid required)
|
||||
* @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} login On type = "password". Login Token
|
||||
* @apiSuccess {String} special On type = "password". Special Token
|
||||
* @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required
|
||||
* @apiSuccess {String} tfa.id The ID 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);
|
||||
|
||||
/**
|
||||
* @api {get} /user/token
|
||||
* @apiName UserGetToken
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Object[]} token
|
||||
* @apiSuccess {String} token.id The Token ID
|
||||
* @apiSuccess {String} token.special Identifies Special Token
|
||||
* @apiSuccess {String} token.ip IP the token was optained from
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @api {delete} /user/token/:id
|
||||
* @apiParam {String} id The id of the token to be deleted
|
||||
*
|
||||
* @apiName UserDeleteToken
|
||||
*
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
UserRoute.delete("/token/:id", DeleteToken);
|
||||
|
||||
/**
|
||||
* @api {delete} /user/account
|
||||
* @apiName UserGetAccount
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
* @apiSuccess {Object[]} user
|
||||
* @apiSuccess {String} user.id User ID
|
||||
* @apiSuccess {String} user.name Full name of the user
|
||||
* @apiSuccess {String} user.username Username of user
|
||||
* @apiSuccess {Date} user.birthday Birthday
|
||||
* @apiSuccess {Number} user.gender Gender of user (none = 0, male = 1, female = 2, other = 3)
|
||||
*/
|
||||
UserRoute.get("/account", GetAccount);
|
||||
|
||||
/**
|
||||
* @api {delete} /user/account
|
||||
* @apiName UserGetAccount
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
* @apiSuccess {Object} contact
|
||||
* @apiSuccess {Object[]} user.mail EMail addresses
|
||||
* @apiSuccess {Object[]} user.phone Phone numbers
|
||||
*/
|
||||
UserRoute.get("/contact", GetContactInfos);
|
||||
|
||||
UserRoute.use("/oauth", OAuthRoute);
|
||||
|
||||
export default UserRoute;
|
||||
import { Router } from "express";
|
||||
import Register from "./register";
|
||||
import OAuthRoute from "./oauth";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
|
||||
/**
|
||||
* @api {post} /user/register
|
||||
* @apiName UserRegister
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiParam {String} mail EMail linked to this Account
|
||||
* @apiParam {String} username The new Username
|
||||
* @apiParam {String} password Password hashed and salted like specification
|
||||
* @apiParam {String} salt The Salt used for password hashing
|
||||
* @apiParam {String} regcode The regcode, that should be used
|
||||
* @apiParam {String} gender Gender can be: "male", "female", "other", "none"
|
||||
* @apiParam {String} name The real name of the User
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*
|
||||
* @apiErrorExample {Object} Error-Response:
|
||||
{
|
||||
error: [
|
||||
{
|
||||
message: "Some Error",
|
||||
field: "username"
|
||||
}
|
||||
],
|
||||
status: 400
|
||||
}
|
||||
*/
|
||||
UserRoute.post("/register", Register);
|
||||
|
||||
UserRoute.use("/oauth", OAuthRoute);
|
||||
|
||||
export default UserRoute;
|
||||
|
@ -1,134 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import User, { IUser } from "../../models/user";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import LoginToken from "../../models/login_token";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
|
||||
import * as crypto from "crypto";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let type = req.query.type as string;
|
||||
if (type === "username") {
|
||||
let { username, uid } = req.query as { [key: string]: string };
|
||||
let user = await User.findOne(
|
||||
username ? { username: username.toLowerCase() } : { uid: uid }
|
||||
);
|
||||
if (!user) {
|
||||
res.json({ error: req.__("User not found") });
|
||||
} else {
|
||||
res.json({ salt: user.salt, uid: user.uid });
|
||||
}
|
||||
return;
|
||||
} else if (type === "password") {
|
||||
const sendToken = async (user: IUser, tfa?: any[]) => {
|
||||
let ip =
|
||||
req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
||||
let client = {
|
||||
ip: Array.isArray(ip) ? ip[0] : ip,
|
||||
browser: req.headers["user-agent"],
|
||||
};
|
||||
|
||||
let token_str = randomBytes(16).toString("hex");
|
||||
let tfa_exp = moment().add(5, "minutes").toDate();
|
||||
let token_exp = moment().add(6, "months").toDate();
|
||||
let token = LoginToken.new({
|
||||
token: token_str,
|
||||
valid: true,
|
||||
validTill: tfa ? tfa_exp : token_exp,
|
||||
user: user._id,
|
||||
validated: tfa ? false : true,
|
||||
...client,
|
||||
});
|
||||
await LoginToken.save(token);
|
||||
|
||||
let special_str = randomBytes(24).toString("hex");
|
||||
let special_exp = moment().add(30, "minutes").toDate();
|
||||
let special = LoginToken.new({
|
||||
token: special_str,
|
||||
valid: true,
|
||||
validTill: tfa ? tfa_exp : special_exp,
|
||||
special: true,
|
||||
user: user._id,
|
||||
validated: tfa ? false : true,
|
||||
...client,
|
||||
});
|
||||
await LoginToken.save(special);
|
||||
|
||||
res.json({
|
||||
login: { token: token_str, expires: token.validTill.toUTCString() },
|
||||
special: {
|
||||
token: special_str,
|
||||
expires: special.validTill.toUTCString(),
|
||||
},
|
||||
tfa,
|
||||
});
|
||||
};
|
||||
|
||||
let { username, password, uid, date } = req.body;
|
||||
|
||||
let user = await User.findOne(
|
||||
username ? { username: username.toLowerCase() } : { uid: uid }
|
||||
);
|
||||
if (!user) {
|
||||
res.json({ error: req.__("User not found") });
|
||||
} else {
|
||||
let upw = user.password;
|
||||
if (date) {
|
||||
if (
|
||||
!moment(date).isBetween(
|
||||
moment().subtract(1, "minute"),
|
||||
moment().add(1, "minute")
|
||||
)
|
||||
) {
|
||||
res.json({
|
||||
error: req.__(
|
||||
"Invalid timestamp. Please check your devices time!"
|
||||
),
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
upw = crypto
|
||||
.createHash("sha512")
|
||||
.update(upw + date.toString())
|
||||
.digest("hex");
|
||||
}
|
||||
}
|
||||
if (upw !== password) {
|
||||
res.json({ error: req.__("Password or username wrong") });
|
||||
} else {
|
||||
let twofactor = await TwoFactor.find({
|
||||
user: 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);
|
||||
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;
|
@ -1,38 +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 });
|
||||
}
|
||||
);
|
||||
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 });
|
||||
}
|
||||
);
|
||||
|
@ -1,45 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
);
|
@ -1,100 +0,0 @@
|
||||
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.TOTP,
|
||||
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.TOTP
|
||||
) {
|
||||
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,16 +0,0 @@
|
||||
import LoginToken, { ILoginToken } from "../../../models/login_token";
|
||||
import moment = require("moment");
|
||||
|
||||
export async function upgradeToken(token: ILoginToken) {
|
||||
token.data = undefined;
|
||||
token.valid = true;
|
||||
token.validated = true;
|
||||
//TODO durations from config
|
||||
let expires = (token.special
|
||||
? moment().add(30, "minute")
|
||||
: moment().add(6, "months")
|
||||
).toDate();
|
||||
token.validTill = expires;
|
||||
await LoginToken.save(token);
|
||||
return expires;
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
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,135 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||
import TwoFactor, {
|
||||
TFATypes as TwoFATypes,
|
||||
IOTC,
|
||||
} from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import { upgradeToken } from "../helper";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import * as qrcode from "qrcode";
|
||||
import config from "../../../../config";
|
||||
|
||||
const OTCRoute = Router();
|
||||
|
||||
OTCRoute.post(
|
||||
"/",
|
||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (type === "create") {
|
||||
//Generating new
|
||||
let secret = speakeasy.generateSecret({
|
||||
name: config.core.name,
|
||||
issuer: config.core.name,
|
||||
});
|
||||
let twofactor = TwoFactor.new(<IOTC>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.TOTP,
|
||||
valid: false,
|
||||
data: secret.base32,
|
||||
});
|
||||
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({
|
||||
image: dataurl,
|
||||
id: twofactor._id,
|
||||
});
|
||||
} else if (type === "validate") {
|
||||
// Checking code and marking as valid
|
||||
const { code, id } = req.body;
|
||||
Logging.debug(req.body, id);
|
||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||
const err = () => {
|
||||
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
||||
};
|
||||
if (
|
||||
!twofactor ||
|
||||
!twofactor.user.equals(req.user._id) ||
|
||||
twofactor.type !== TwoFATypes.TOTP ||
|
||||
!twofactor.data ||
|
||||
twofactor.valid
|
||||
) {
|
||||
Logging.debug("Not found or wrong user", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
||||
await TwoFactor.delete(twofactor);
|
||||
Logging.debug("Expired!", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
twofactor.expires = undefined;
|
||||
twofactor.valid = true;
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
} else {
|
||||
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
OTCRoute.put(
|
||||
"/",
|
||||
Stacker(
|
||||
GetUserMiddleware(true, false, undefined, false),
|
||||
async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let { id, code } = req.body;
|
||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||
|
||||
if (
|
||||
!twofactor ||
|
||||
!twofactor.valid ||
|
||||
!twofactor.user.equals(req.user._id) ||
|
||||
twofactor.type !== TwoFATypes.TOTP
|
||||
) {
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
let [login_exp, special_exp] = await Promise.all([
|
||||
upgradeToken(login),
|
||||
upgradeToken(special),
|
||||
]);
|
||||
res.json({ success: true, login_exp, special_exp });
|
||||
} else {
|
||||
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default OTCRoute;
|
@ -1,206 +0,0 @@
|
||||
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.WEBAUTHN,
|
||||
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.WEBAUTHN ||
|
||||
!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.WEBAUTHN,
|
||||
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.WEBAUTHN,
|
||||
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;
|
@ -1,75 +1,81 @@
|
||||
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;
|
||||
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;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
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,
|
||||
secret: {
|
||||
type: String,
|
||||
optional: false,
|
||||
description: "Cookie secret"
|
||||
}
|
||||
},
|
||||
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,13 +1,13 @@
|
||||
import SafeMongo from "@hibas123/safe_mongo";
|
||||
import Config from "./config";
|
||||
let dbname = "openauth";
|
||||
let host = "localhost";
|
||||
if (Config.database) {
|
||||
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,
|
||||
});
|
||||
export default DB;
|
||||
import SafeMongo from "@hibas123/safe_mongo";
|
||||
import Config from "./config";
|
||||
let dbname = "openauth";
|
||||
let host = "localhost";
|
||||
if (Config.database) {
|
||||
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, {
|
||||
|
||||
});
|
||||
export default DB;
|
||||
|
39
Backend/src/express.d.ts
vendored
39
Backend/src/express.d.ts
vendored
@ -1,16 +1,23 @@
|
||||
import { IUser } from "./models/user";
|
||||
import { IClient } from "./models/client";
|
||||
import { ILoginToken } from "./models/login_token";
|
||||
|
||||
declare module "express" {
|
||||
interface Request {
|
||||
user: IUser;
|
||||
client: IClient;
|
||||
isAdmin: boolean;
|
||||
special: boolean;
|
||||
token: {
|
||||
login: ILoginToken;
|
||||
special?: ILoginToken;
|
||||
};
|
||||
}
|
||||
}
|
||||
import { IUser } from "./models/user";
|
||||
import { IClient } from "./models/client";
|
||||
|
||||
declare module "express" {
|
||||
interface Request {
|
||||
user: IUser;
|
||||
client: IClient;
|
||||
isAdmin: boolean;
|
||||
special: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
user_id: string;
|
||||
validated: boolean;
|
||||
login_state: {
|
||||
username: string;
|
||||
password_correct: boolean;
|
||||
webauthn_challenge?: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +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,
|
||||
}
|
||||
);
|
||||
}
|
||||
import { IUser, Gender } from "../models/user";
|
||||
import { ObjectId } from "bson";
|
||||
import { createJWT } from "../keys";
|
||||
import { IClient } from "../models/client";
|
||||
import config from "../config";
|
||||
import moment = require("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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
29
Backend/src/helper/login.ts
Normal file
29
Backend/src/helper/login.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { SessionContext } from "../api/jrpc";
|
||||
|
||||
export function requireLoginState(ctx: SessionContext, validated: boolean = true, special: boolean = false): boolean {
|
||||
if (!ctx.user) return false;
|
||||
if (validated && !ctx.session.validated) return false;
|
||||
|
||||
if (special) {
|
||||
//TODO: Implement something...
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function RequireLogin(validated = true, special = false) {
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
let original = descriptor.value;
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
let ctx = args[args.length - 1] as SessionContext;
|
||||
if (!ctx) throw new Error("Invalid request");
|
||||
|
||||
if (!requireLoginState(ctx, validated, special)) {
|
||||
throw new Error("Not logged in");
|
||||
}
|
||||
|
||||
return original.apply(this, args);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,69 +1,69 @@
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import * as fs from "fs";
|
||||
|
||||
let private_key: string;
|
||||
let rsa: RSA;
|
||||
export function sign(message: Buffer): Buffer {
|
||||
return rsa.sign(message, "buffer");
|
||||
}
|
||||
|
||||
export function verify(message: Buffer, signature: Buffer): boolean {
|
||||
return rsa.verify(message, signature);
|
||||
}
|
||||
|
||||
export let public_key: string;
|
||||
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import config from "./config";
|
||||
|
||||
export function createJWT(payload: any, options: jwt.SignOptions) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
return jwt.sign(payload, private_key, options, (err, token) => {
|
||||
if (err) reject(err);
|
||||
else resolve(token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function validateJWT(data: string) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
jwt.verify(data, public_key, (err, valid) => {
|
||||
if (err) reject(err);
|
||||
else resolve(valid);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let create = false;
|
||||
if (fs.existsSync("./keys")) {
|
||||
if (fs.existsSync("./keys/private.pem")) {
|
||||
if (fs.existsSync("./keys/public.pem")) {
|
||||
Logging.log("Using existing private and public key");
|
||||
private_key = fs.readFileSync("./keys/private.pem").toString("utf8");
|
||||
public_key = fs.readFileSync("./keys/public.pem").toString("utf8");
|
||||
|
||||
if (!private_key || !public_key) {
|
||||
create = true;
|
||||
}
|
||||
} else create = true;
|
||||
} else create = true;
|
||||
} else create = true;
|
||||
|
||||
import * as RSA from "node-rsa";
|
||||
|
||||
if (create === true) {
|
||||
Logging.log("Started RSA Key gen");
|
||||
let rsa = new RSA({ b: 4096 });
|
||||
private_key = rsa.exportKey("private");
|
||||
public_key = rsa.exportKey("public");
|
||||
|
||||
if (!fs.existsSync("./keys")) {
|
||||
fs.mkdirSync("./keys");
|
||||
}
|
||||
fs.writeFileSync("./keys/private.pem", private_key);
|
||||
fs.writeFileSync("./keys/public.pem", public_key);
|
||||
Logging.log("Key pair generated");
|
||||
}
|
||||
|
||||
rsa = new RSA(private_key, "private");
|
||||
rsa.importKey(public_key, "public");
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import * as fs from "fs";
|
||||
|
||||
let private_key: string;
|
||||
let rsa: RSA;
|
||||
export function sign(message: Buffer): Buffer {
|
||||
return rsa.sign(message, "buffer");
|
||||
}
|
||||
|
||||
export function verify(message: Buffer, signature: Buffer): boolean {
|
||||
return rsa.verify(message, signature);
|
||||
}
|
||||
|
||||
export let public_key: string;
|
||||
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import config from "./config";
|
||||
|
||||
export function createJWT(payload: any, options: jwt.SignOptions) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
return jwt.sign(payload, private_key, options, (err, token) => {
|
||||
if (err) reject(err);
|
||||
else resolve(token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function validateJWT(data: string) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
jwt.verify(data, public_key, (err, valid) => {
|
||||
if (err) reject(err);
|
||||
else resolve(valid);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let create = false;
|
||||
if (fs.existsSync("./keys")) {
|
||||
if (fs.existsSync("./keys/private.pem")) {
|
||||
if (fs.existsSync("./keys/public.pem")) {
|
||||
Logging.log("Using existing private and public key");
|
||||
private_key = fs.readFileSync("./keys/private.pem").toString("utf8");
|
||||
public_key = fs.readFileSync("./keys/public.pem").toString("utf8");
|
||||
|
||||
if (!private_key || !public_key) {
|
||||
create = true;
|
||||
}
|
||||
} else create = true;
|
||||
} else create = true;
|
||||
} else create = true;
|
||||
|
||||
import RSA from "node-rsa";
|
||||
|
||||
if (create === true) {
|
||||
Logging.log("Started RSA Key gen");
|
||||
let rsa = new RSA({ b: 4096 });
|
||||
private_key = rsa.exportKey("private");
|
||||
public_key = rsa.exportKey("public");
|
||||
|
||||
if (!fs.existsSync("./keys")) {
|
||||
fs.mkdirSync("./keys");
|
||||
}
|
||||
fs.writeFileSync("./keys/private.pem", private_key);
|
||||
fs.writeFileSync("./keys/public.pem", public_key);
|
||||
Logging.log("Key pair generated");
|
||||
}
|
||||
|
||||
rsa = new RSA(private_key, "private");
|
||||
rsa.importKey(public_key, "public");
|
||||
|
@ -1,40 +1,40 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "mongodb";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export interface IClient extends ModelDataBase {
|
||||
maintainer: ObjectID;
|
||||
internal: boolean;
|
||||
name: string;
|
||||
redirect_url: string;
|
||||
website: string;
|
||||
logo?: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
featured?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const Client = DB.addModel<IClient>({
|
||||
name: "client",
|
||||
versions: [
|
||||
{
|
||||
migration: () => {},
|
||||
schema: {
|
||||
maintainer: { type: ObjectID },
|
||||
internal: { type: Boolean, default: false },
|
||||
name: { type: String },
|
||||
redirect_url: { type: String },
|
||||
website: { type: String },
|
||||
logo: { type: String, optional: true },
|
||||
client_id: { type: String, default: () => v4() },
|
||||
client_secret: { type: String },
|
||||
featured: { type: Boolean, optional: true },
|
||||
description: { type: String, optional: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default Client;
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export interface IClient extends ModelDataBase {
|
||||
maintainer: ObjectId;
|
||||
internal: boolean;
|
||||
name: string;
|
||||
redirect_url: string;
|
||||
website: string;
|
||||
logo?: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
featured?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const Client = DB.addModel<IClient>({
|
||||
name: "client",
|
||||
versions: [
|
||||
{
|
||||
migration: () => { },
|
||||
schema: {
|
||||
maintainer: { type: ObjectId },
|
||||
internal: { type: Boolean, default: false },
|
||||
name: { type: String },
|
||||
redirect_url: { type: String },
|
||||
website: { type: String },
|
||||
logo: { type: String, optional: true },
|
||||
client_id: { type: String, default: () => v4() },
|
||||
client_secret: { type: String },
|
||||
featured: { type: Boolean, optional: true },
|
||||
description: { type: String, optional: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default Client;
|
||||
|
@ -1,29 +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;
|
||||
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;
|
||||
|
@ -1,25 +1,25 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "mongodb";
|
||||
|
||||
export interface IGrant extends ModelDataBase {
|
||||
user: ObjectID;
|
||||
client: ObjectID;
|
||||
permissions: ObjectID[];
|
||||
}
|
||||
|
||||
const Grant = DB.addModel<IGrant>({
|
||||
name: "grant",
|
||||
versions: [
|
||||
{
|
||||
migration: () => {},
|
||||
schema: {
|
||||
user: { type: ObjectID },
|
||||
client: { type: ObjectID },
|
||||
permissions: { type: ObjectID, array: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default Grant;
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export interface IGrant extends ModelDataBase {
|
||||
user: ObjectId;
|
||||
client: ObjectId;
|
||||
permissions: ObjectId[];
|
||||
}
|
||||
|
||||
const Grant = DB.addModel<IGrant>({
|
||||
name: "grant",
|
||||
versions: [
|
||||
{
|
||||
migration: () => { },
|
||||
schema: {
|
||||
user: { type: ObjectId },
|
||||
client: { type: ObjectId },
|
||||
permissions: { type: ObjectId, array: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default Grant;
|
||||
|
@ -1,76 +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;
|
||||
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;
|
||||
|
@ -1,37 +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;
|
||||
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;
|
||||
|
@ -1,32 +1,32 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "mongodb";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export interface IRefreshToken extends ModelDataBase {
|
||||
token: string;
|
||||
user: ObjectID;
|
||||
client: ObjectID;
|
||||
permissions: ObjectID[];
|
||||
validTill: Date;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
const RefreshToken = DB.addModel<IRefreshToken>({
|
||||
name: "refresh_token",
|
||||
versions: [
|
||||
{
|
||||
migration: () => {},
|
||||
schema: {
|
||||
token: { type: String },
|
||||
user: { type: ObjectID },
|
||||
client: { type: ObjectID },
|
||||
permissions: { type: Array },
|
||||
validTill: { type: Date },
|
||||
valid: { type: Boolean },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default RefreshToken;
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export interface IRefreshToken extends ModelDataBase {
|
||||
token: string;
|
||||
user: ObjectId;
|
||||
client: ObjectId;
|
||||
permissions: ObjectId[];
|
||||
validTill: Date;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
const RefreshToken = DB.addModel<IRefreshToken>({
|
||||
name: "refresh_token",
|
||||
versions: [
|
||||
{
|
||||
migration: () => { },
|
||||
schema: {
|
||||
token: { type: String },
|
||||
user: { type: ObjectId },
|
||||
client: { type: ObjectId },
|
||||
permissions: { type: Array },
|
||||
validTill: { type: Date },
|
||||
valid: { type: Boolean },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default RefreshToken;
|
||||
|
@ -1,57 +1,57 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "mongodb";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export interface IRegCode extends ModelDataBase {
|
||||
token: string;
|
||||
valid: boolean;
|
||||
validTill: Date;
|
||||
}
|
||||
|
||||
const RegCode = DB.addModel<IRegCode>({
|
||||
name: "reg_code",
|
||||
versions: [
|
||||
{
|
||||
migration: () => {},
|
||||
schema: {
|
||||
token: { type: String },
|
||||
valid: { type: Boolean },
|
||||
validTill: { type: Date },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default RegCode;
|
||||
|
||||
// import { Model, Table, Column, ForeignKey, BelongsTo, Unique, CreatedAt, UpdatedAt, DeletedAt, HasMany, BelongsToMany, Default, DataType } from "sequelize-typescript"
|
||||
// import User from "./user";
|
||||
// import Permission from "./permissions";
|
||||
// import RefreshPermission from "./refresh_permission";
|
||||
|
||||
// @Table
|
||||
// export default class RegCode extends Model<RegCode> {
|
||||
// @Unique
|
||||
// @Default(DataType.UUIDV4)
|
||||
// @Column(DataType.UUID)
|
||||
// token: string
|
||||
|
||||
// @Column
|
||||
// validTill: Date
|
||||
|
||||
// @Column
|
||||
// valid: boolean
|
||||
|
||||
// @Column
|
||||
// @CreatedAt
|
||||
// creationDate: Date;
|
||||
|
||||
// @Column
|
||||
// @UpdatedAt
|
||||
// updatedOn: Date;
|
||||
|
||||
// @Column
|
||||
// @DeletedAt
|
||||
// deletionDate: Date;
|
||||
// }
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export interface IRegCode extends ModelDataBase {
|
||||
token: string;
|
||||
valid: boolean;
|
||||
validTill: Date;
|
||||
}
|
||||
|
||||
const RegCode = DB.addModel<IRegCode>({
|
||||
name: "reg_code",
|
||||
versions: [
|
||||
{
|
||||
migration: () => { },
|
||||
schema: {
|
||||
token: { type: String },
|
||||
valid: { type: Boolean },
|
||||
validTill: { type: Date },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default RegCode;
|
||||
|
||||
// import { Model, Table, Column, ForeignKey, BelongsTo, Unique, CreatedAt, UpdatedAt, DeletedAt, HasMany, BelongsToMany, Default, DataType } from "sequelize-typescript"
|
||||
// import User from "./user";
|
||||
// import Permission from "./permissions";
|
||||
// import RefreshPermission from "./refresh_permission";
|
||||
|
||||
// @Table
|
||||
// export default class RegCode extends Model<RegCode> {
|
||||
// @Unique
|
||||
// @Default(DataType.UUIDV4)
|
||||
// @Column(DataType.UUID)
|
||||
// token: string
|
||||
|
||||
// @Column
|
||||
// validTill: Date
|
||||
|
||||
// @Column
|
||||
// valid: boolean
|
||||
|
||||
// @Column
|
||||
// @CreatedAt
|
||||
// creationDate: Date;
|
||||
|
||||
// @Column
|
||||
// @UpdatedAt
|
||||
// updatedOn: Date;
|
||||
|
||||
// @Column
|
||||
// @DeletedAt
|
||||
// deletionDate: Date;
|
||||
// }
|
||||
|
@ -1,69 +1,71 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "bson";
|
||||
|
||||
export enum TFATypes {
|
||||
TOTP,
|
||||
BACKUP_CODE,
|
||||
WEBAUTHN,
|
||||
APP_ALLOW,
|
||||
}
|
||||
|
||||
export const TFANames = new Map<TFATypes, string>();
|
||||
TFANames.set(TFATypes.TOTP, "Authenticator");
|
||||
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
|
||||
TFANames.set(TFATypes.WEBAUTHN, "Security Key (WebAuthn)");
|
||||
TFANames.set(TFATypes.APP_ALLOW, "App Push");
|
||||
|
||||
export interface ITwoFactor extends ModelDataBase {
|
||||
user: ObjectID;
|
||||
valid: boolean;
|
||||
expires?: Date;
|
||||
name?: string;
|
||||
type: TFATypes;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface IOTC extends ITwoFactor {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface IYubiKey extends ITwoFactor {
|
||||
data: {
|
||||
registration?: any;
|
||||
publicKey: string;
|
||||
keyHandle: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IU2F extends ITwoFactor {
|
||||
data: {
|
||||
challenge?: string;
|
||||
publicKey: string;
|
||||
keyHandle: string;
|
||||
registration?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBackupCode extends ITwoFactor {
|
||||
data: string[];
|
||||
}
|
||||
|
||||
const TwoFactor = DB.addModel<ITwoFactor>({
|
||||
name: "twofactor",
|
||||
versions: [
|
||||
{
|
||||
migration: (e) => { },
|
||||
schema: {
|
||||
user: { type: ObjectID },
|
||||
valid: { type: Boolean },
|
||||
expires: { type: Date, optional: true },
|
||||
name: { type: String, optional: true },
|
||||
type: { type: Number },
|
||||
data: { type: "any" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default TwoFactor;
|
||||
import { TFAType } from "@hibas123/openauth-internalapi";
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectId } from "bson";
|
||||
import { Binary } from "mongodb";
|
||||
|
||||
export { TFAType as TFATypes };
|
||||
|
||||
|
||||
export const TFANames = new Map<TFAType, string>();
|
||||
TFANames.set(TFAType.TOTP, "Authenticator");
|
||||
TFANames.set(TFAType.BACKUP_CODE, "Backup Codes");
|
||||
TFANames.set(TFAType.WEBAUTHN, "Security Key (WebAuthn)");
|
||||
TFANames.set(TFAType.APP_ALLOW, "App Push");
|
||||
|
||||
export interface ITwoFactor extends ModelDataBase {
|
||||
user: ObjectId;
|
||||
valid: boolean;
|
||||
expires?: Date;
|
||||
name?: string;
|
||||
type: TFAType;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface ITOTP extends ITwoFactor {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface IWebAuthn extends ITwoFactor {
|
||||
data: {
|
||||
challenge?: any;
|
||||
device?: {
|
||||
credentialID: Binary;
|
||||
credentialPublicKey: Binary;
|
||||
counter: number;
|
||||
transports: AuthenticatorTransport[]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface IU2F extends ITwoFactor {
|
||||
data: {
|
||||
challenge?: string;
|
||||
publicKey: string;
|
||||
keyHandle: string;
|
||||
registration?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBackupCode extends ITwoFactor {
|
||||
data: string[];
|
||||
}
|
||||
|
||||
const TwoFactor = DB.addModel<ITwoFactor>({
|
||||
name: "twofactor",
|
||||
versions: [
|
||||
{
|
||||
migration: (e) => { },
|
||||
schema: {
|
||||
user: { type: ObjectId },
|
||||
valid: { type: Boolean },
|
||||
expires: { type: Date, optional: true },
|
||||
name: { type: String, optional: true },
|
||||
type: { type: Number },
|
||||
data: { type: "any" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default TwoFactor;
|
||||
|
@ -1,134 +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;
|
||||
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,147 +1,170 @@
|
||||
import User, { Gender } from "./models/user";
|
||||
import Client from "./models/client";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import RegCode from "./models/regcodes";
|
||||
import * as moment from "moment";
|
||||
import Permission from "./models/permissions";
|
||||
import { ObjectID } from "bson";
|
||||
import DB from "./database";
|
||||
import TwoFactor from "./models/twofactor";
|
||||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import LoginToken from "./models/login_token";
|
||||
import Mail from "./models/mail";
|
||||
|
||||
export default async function TestData() {
|
||||
Logging.warn("Running in dev mode! Database will be cleared!");
|
||||
await DB.db.dropDatabase();
|
||||
|
||||
let mail = await Mail.findOne({ mail: "test@test.de" });
|
||||
if (!mail) {
|
||||
mail = Mail.new({
|
||||
mail: "test@test.de",
|
||||
primary: true,
|
||||
verified: true,
|
||||
});
|
||||
|
||||
await Mail.save(mail);
|
||||
}
|
||||
|
||||
let u = await User.findOne({ username: "test" });
|
||||
if (!u) {
|
||||
Logging.log("Adding test user");
|
||||
u = User.new({
|
||||
username: "test",
|
||||
birthday: new Date(),
|
||||
gender: Gender.male,
|
||||
name: "Test Test",
|
||||
password:
|
||||
"125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
|
||||
salt: "test",
|
||||
admin: true,
|
||||
phones: [
|
||||
{ phone: "+4915962855955", primary: true, verified: true },
|
||||
{ phone: "+4915962855932", primary: false, verified: false },
|
||||
],
|
||||
mails: [mail._id],
|
||||
});
|
||||
await User.save(u);
|
||||
}
|
||||
|
||||
let c = await Client.findOne({ client_id: "test001" });
|
||||
if (!c) {
|
||||
Logging.log("Adding test client");
|
||||
c = Client.new({
|
||||
client_id: "test001",
|
||||
client_secret: "test001",
|
||||
internal: true,
|
||||
maintainer: u._id,
|
||||
name: "Test Client",
|
||||
website: "http://example.com",
|
||||
redirect_url: "http://example.com",
|
||||
featured: true,
|
||||
description:
|
||||
"This client is just for testing purposes. It does not have any functionality.",
|
||||
});
|
||||
await Client.save(c);
|
||||
}
|
||||
|
||||
let perm = await Permission.findById("507f1f77bcf86cd799439011");
|
||||
if (!perm) {
|
||||
Logging.log("Adding test permission");
|
||||
perm = Permission.new({
|
||||
_id: new ObjectID("507f1f77bcf86cd799439011"),
|
||||
name: "TestPerm",
|
||||
description: "Permission just for testing purposes",
|
||||
client: c._id,
|
||||
});
|
||||
|
||||
await (await (Permission as any)._collection).insertOne(perm);
|
||||
|
||||
// Permission.save(perm);
|
||||
}
|
||||
|
||||
let r = await RegCode.findOne({ token: "test" });
|
||||
if (!r) {
|
||||
Logging.log("Adding test reg_code");
|
||||
r = RegCode.new({
|
||||
token: "test",
|
||||
valid: true,
|
||||
validTill: moment().add("1", "year").toDate(),
|
||||
});
|
||||
await RegCode.save(r);
|
||||
}
|
||||
|
||||
let t = await TwoFactor.findOne({ user: u._id, type: 0 });
|
||||
if (!t) {
|
||||
t = TwoFactor.new({
|
||||
user: u._id,
|
||||
name: "Test OTP",
|
||||
type: 0,
|
||||
valid: true,
|
||||
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
||||
expires: null,
|
||||
});
|
||||
await TwoFactor.save(t);
|
||||
}
|
||||
|
||||
let login_token = await LoginToken.findOne({ token: "test01" });
|
||||
if (login_token) await LoginToken.delete(login_token);
|
||||
|
||||
login_token = LoginToken.new({
|
||||
browser: "DEMO",
|
||||
ip: "10.0.0.1",
|
||||
special: false,
|
||||
token: "test01",
|
||||
valid: true,
|
||||
validTill: moment().add("10", "years").toDate(),
|
||||
user: u._id,
|
||||
validated: true,
|
||||
});
|
||||
await LoginToken.save(login_token);
|
||||
|
||||
let special_token = await LoginToken.findOne({ token: "test02" });
|
||||
if (special_token) await LoginToken.delete(special_token);
|
||||
|
||||
special_token = LoginToken.new({
|
||||
browser: "DEMO",
|
||||
ip: "10.0.0.1",
|
||||
special: true,
|
||||
token: "test02",
|
||||
valid: true,
|
||||
validTill: moment().add("10", "years").toDate(),
|
||||
user: u._id,
|
||||
validated: true,
|
||||
});
|
||||
await LoginToken.save(special_token);
|
||||
|
||||
// setInterval(() => {
|
||||
// let code = speakeasy.totp({
|
||||
// secret: t.data,
|
||||
// encoding: "base32"
|
||||
// })
|
||||
// Logging.debug("OTC Code is:", code);
|
||||
// }, 1000)
|
||||
|
||||
console.log("Finished adding test data")
|
||||
}
|
||||
import User, { Gender } from "./models/user";
|
||||
import Client from "./models/client";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import RegCode from "./models/regcodes";
|
||||
import moment from "moment";
|
||||
import Permission from "./models/permissions";
|
||||
import { ObjectId } from "mongodb";
|
||||
import DB from "./database";
|
||||
import TwoFactor from "./models/twofactor";
|
||||
|
||||
import LoginToken from "./models/login_token";
|
||||
import Mail from "./models/mail";
|
||||
|
||||
export default async function TestData() {
|
||||
Logging.warn("Running in dev mode! Database will be cleared!");
|
||||
// await DB.db.dropDatabase();
|
||||
|
||||
let mail = await Mail.findOne({ mail: "test@test.de" });
|
||||
if (!mail) {
|
||||
mail = Mail.new({
|
||||
mail: "test@test.de",
|
||||
primary: true,
|
||||
verified: true,
|
||||
});
|
||||
|
||||
await Mail.save(mail);
|
||||
}
|
||||
|
||||
let u = await User.findOne({ username: "test" });
|
||||
if (!u) {
|
||||
Logging.log("Adding test user");
|
||||
u = User.new({
|
||||
username: "test",
|
||||
birthday: new Date(),
|
||||
gender: Gender.male,
|
||||
name: "Test Test",
|
||||
password:
|
||||
"125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
|
||||
salt: "test",
|
||||
admin: true,
|
||||
phones: [
|
||||
{ phone: "+4915962855955", primary: true, verified: true },
|
||||
{ phone: "+4915962855932", primary: false, verified: false },
|
||||
],
|
||||
mails: [mail._id],
|
||||
});
|
||||
await User.save(u);
|
||||
}
|
||||
|
||||
let c = await Client.findOne({ client_id: "test001" });
|
||||
if (!c) {
|
||||
Logging.log("Adding test client");
|
||||
c = Client.new({
|
||||
client_id: "test001",
|
||||
client_secret: "test001",
|
||||
internal: true,
|
||||
maintainer: u._id,
|
||||
name: "Test Client",
|
||||
website: "http://example.com",
|
||||
redirect_url: "http://example.com",
|
||||
featured: true,
|
||||
description:
|
||||
"This client is just for testing purposes. It does not have any functionality.",
|
||||
});
|
||||
await Client.save(c);
|
||||
}
|
||||
|
||||
let perm = await Permission.findById("507f1f77bcf86cd799439011");
|
||||
if (!perm) {
|
||||
Logging.log("Adding test permission");
|
||||
perm = Permission.new({
|
||||
_id: new ObjectId("507f1f77bcf86cd799439011"),
|
||||
name: "TestPerm",
|
||||
description: "Permission just for testing purposes",
|
||||
client: c._id,
|
||||
});
|
||||
|
||||
await (await (Permission as any)._collection).insertOne(perm);
|
||||
|
||||
// Permission.save(perm);
|
||||
}
|
||||
|
||||
let r = await RegCode.findOne({ token: "test" });
|
||||
if (!r) {
|
||||
Logging.log("Adding test reg_code");
|
||||
r = RegCode.new({
|
||||
token: "test",
|
||||
valid: true,
|
||||
validTill: moment().add("1", "year").toDate(),
|
||||
});
|
||||
await RegCode.save(r);
|
||||
}
|
||||
|
||||
let t = await TwoFactor.findOne({ user: u._id, type: 0 });
|
||||
if (!t) {
|
||||
Logging.log("Adding test TOTP")
|
||||
t = TwoFactor.new({
|
||||
user: u._id,
|
||||
name: "Test OTP",
|
||||
type: 0,
|
||||
valid: true,
|
||||
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
||||
expires: null,
|
||||
});
|
||||
await TwoFactor.save(t);
|
||||
}
|
||||
|
||||
// let tw = await TwoFactor.findOne({ user: u._id, type: 2 });
|
||||
// if (!tw) {
|
||||
// Logging.log("Adding test WebAuthn")
|
||||
// tw = TwoFactor.new({
|
||||
// user: u._id,
|
||||
// name: "WebAuthn",
|
||||
// type: 2,
|
||||
// valid: true,
|
||||
// data: {
|
||||
// device: {
|
||||
// credentialPublicKey: Buffer.from("pQECAyYgASFYINiHCRopJIn1GoTXq7SpDTJR1nzocqOWhjvpYaKLzzhSIlggvuHhjABe8NxbOIGA11vrd5deUT5R30anpE7W7xzPcsk=", "base64"),
|
||||
// credentialID: Buffer.from("i/BJiffx0bxjQ9Ptyvc9ORELXALxrvD6pad1Xc/2nDI=", "base64"),
|
||||
// counter: 1,
|
||||
// transports: [
|
||||
// "usb"
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// await TwoFactor.save(tw);
|
||||
// }
|
||||
|
||||
let login_token = await LoginToken.findOne({ token: "test01" });
|
||||
if (login_token) await LoginToken.delete(login_token);
|
||||
|
||||
login_token = LoginToken.new({
|
||||
browser: "DEMO",
|
||||
ip: "10.0.0.1",
|
||||
special: false,
|
||||
token: "test01",
|
||||
valid: true,
|
||||
validTill: moment().add("10", "years").toDate(),
|
||||
user: u._id,
|
||||
validated: true,
|
||||
});
|
||||
await LoginToken.save(login_token);
|
||||
|
||||
let special_token = await LoginToken.findOne({ token: "test02" });
|
||||
if (special_token) await LoginToken.delete(special_token);
|
||||
|
||||
special_token = LoginToken.new({
|
||||
browser: "DEMO",
|
||||
ip: "10.0.0.1",
|
||||
special: true,
|
||||
token: "test02",
|
||||
valid: true,
|
||||
validTill: moment().add("10", "years").toDate(),
|
||||
user: u._id,
|
||||
validated: true,
|
||||
});
|
||||
await LoginToken.save(special_token);
|
||||
|
||||
// setInterval(() => {
|
||||
// let code = speakeasy.totp({
|
||||
// secret: t.data,
|
||||
// encoding: "base32"
|
||||
// })
|
||||
// Logging.debug("OTC Code is:", code);
|
||||
// }, 1000)
|
||||
|
||||
Logging.log("Finished adding test data");
|
||||
}
|
||||
|
||||
|
@ -1,121 +1,121 @@
|
||||
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.use(
|
||||
"/static",
|
||||
addCache,
|
||||
ServeStatic(path.join(viewsv2_location, "../static"), { 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;
|
||||
import {
|
||||
IRouter,
|
||||
Request,
|
||||
RequestHandler,
|
||||
Router,
|
||||
static as ServeStatic,
|
||||
} from "express";
|
||||
import * as Handlebars from "handlebars";
|
||||
import moment = require("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.use(
|
||||
"/static",
|
||||
addCache,
|
||||
ServeStatic(path.join(viewsv2_location, "../static"), { 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;
|
@ -1,122 +1,175 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
import config, { WebConfig } from "./config";
|
||||
import express from "express";
|
||||
import { Express } from "express";
|
||||
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import { Format } from "@hibas123/logging";
|
||||
|
||||
import bodyparser from "body-parser";
|
||||
import cookieparser from "cookie-parser";
|
||||
import session from "express-session";
|
||||
import MongoStore from "connect-mongo";
|
||||
|
||||
import i18n from "i18n";
|
||||
import compression from "compression";
|
||||
import ApiRouter from "./api";
|
||||
import ViewRouter from "./views";
|
||||
import RequestError, { HttpStatusCode } from "./helper/request_error";
|
||||
import DB from "./database";
|
||||
import promiseMiddleware from "./helper/promiseMiddleware";
|
||||
import User from "./models/user";
|
||||
import LoginToken, { CheckToken } from "./models/login_token";
|
||||
|
||||
export default class Web {
|
||||
server: Express;
|
||||
private port: number;
|
||||
|
||||
constructor(config: WebConfig) {
|
||||
this.server = express();
|
||||
this.port = Number(config.port);
|
||||
this.registerMiddleware();
|
||||
this.registerUserSession();
|
||||
this.registerEndpoints();
|
||||
this.registerErrorHandler();
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.server.listen(this.port, () => {
|
||||
Logging.log(`Server listening on port ${this.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
private registerMiddleware() {
|
||||
this.server.use(session({
|
||||
secret: config.core.secret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: MongoStore.create({
|
||||
client: DB.getClient(),
|
||||
dbName: DB.db.databaseName,
|
||||
collectionName: "sessions",
|
||||
autoRemove: "native",
|
||||
touchAfter: 60 * 60 * 24,
|
||||
}),
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 6,
|
||||
secure: !config.core.dev,
|
||||
sameSite: "strict",
|
||||
}
|
||||
}))
|
||||
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 resFormat: (arg: any) => any = (arg) => arg;
|
||||
if (res.statusCode >= 200 && res.statusCode < 300)
|
||||
resFormat = Format.green;
|
||||
//Green
|
||||
else if (res.statusCode === 304 || res.statusCode === 302)
|
||||
resFormat = Format.yellow; //"\x1b[33m";
|
||||
else if (res.statusCode >= 400 && res.statusCode < 500)
|
||||
resFormat = Format.red; // "\x1b[36m";
|
||||
//Cyan
|
||||
else if (res.statusCode >= 500 && res.statusCode < 600)
|
||||
resFormat = Format.cyan //"\x1b[31m"; //Red
|
||||
|
||||
let m = req.method;
|
||||
while (m.length < 4) m += " ";
|
||||
Logging.getChild("HTTP").log(
|
||||
`${m} ${req.originalUrl} ${(req as any).language || ""
|
||||
}`, resFormat(res.statusCode), `- ${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);
|
||||
});
|
||||
}
|
||||
|
||||
private registerUserSession() {
|
||||
this.server.use(promiseMiddleware(async (req, res, next) => {
|
||||
// if (!req.session.user_id) {
|
||||
// if (req.cookies && req.cookies.login) {
|
||||
// let token = await LoginToken.findOne({ token: req.cookies.login, valid: true });
|
||||
// if (await CheckToken(token, true)) {
|
||||
// req.session.user_id = token.user.toString();
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (req.cookies && req.cookies.special) {
|
||||
// let token = await LoginToken.findOne({ token: req.cookies.special, valid: true });
|
||||
// if (await CheckToken(token, true)) {
|
||||
// req.session.user_id = token.user.toString();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (req.session.user_id) {
|
||||
req.user = await User.findById(req.session.user_id);
|
||||
req.isAdmin = req.user.admin;
|
||||
}
|
||||
|
||||
return next();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +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"]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./lib",
|
||||
"strict": false,
|
||||
"preserveWatchOutput": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules/"],
|
||||
"files": ["src/express.d.ts"],
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
@ -11,9 +11,9 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"classnames": "^2.3.2",
|
||||
"cssnano": "^6.0.0",
|
||||
"esbuild": "^0.17.15",
|
||||
"flowbite": "^1.6.4",
|
||||
"flowbite-svelte": "^0.34.7",
|
||||
"esbuild": "^0.17.16",
|
||||
"flowbite": "^1.6.5",
|
||||
"flowbite-svelte": "^0.34.9",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
@ -27,7 +27,7 @@
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^5.0.3"
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build",
|
||||
@ -39,7 +39,9 @@
|
||||
"@hibas123/theme": "^2.0.6",
|
||||
"@hibas123/utils": "^2.2.18",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@simplewebauthn/browser": "^7.2.0",
|
||||
"cleave.js": "^1.6.0",
|
||||
"joi": "^17.9.1",
|
||||
"what-the-pack": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,33 @@
|
||||
export let title: string;
|
||||
export let loading = false;
|
||||
export let hide = false;
|
||||
|
||||
$: console.log({ loading });
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
min-height: 100vh;
|
||||
@ -21,6 +46,7 @@
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
padding-top: 2.5rem;
|
||||
width: 25rem;
|
||||
|
||||
min-height: calc(100px + 2.5rem);
|
||||
min-width: 100px;
|
||||
@ -34,6 +60,7 @@
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
/* padding: 5px 20px; */
|
||||
}
|
||||
|
||||
@ -65,26 +92,3 @@
|
||||
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>
|
||||
|
33
Frontend/src/components/MainNavbar.svelte
Normal file
33
Frontend/src/components/MainNavbar.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
NavBrand,
|
||||
NavHamburger,
|
||||
NavLi,
|
||||
NavUl,
|
||||
Navbar,
|
||||
} from "flowbite-svelte";
|
||||
|
||||
export let sidebarOpen: boolean;
|
||||
export let sidebarOpenVisible: boolean;
|
||||
</script>
|
||||
|
||||
<Navbar let:hidden let:toggle color="form">
|
||||
{#if sidebarOpenVisible}
|
||||
<NavHamburger on:click={() => (sidebarOpen = !sidebarOpen)} />
|
||||
{/if}
|
||||
<NavBrand href="/">
|
||||
<span
|
||||
class="self-center whitespace-nowrap text-xl font-semibold dark:text-white"
|
||||
>
|
||||
OpenAuth
|
||||
</span>
|
||||
</NavBrand>
|
||||
<NavHamburger on:click={toggle} />
|
||||
<NavUl {hidden}>
|
||||
<NavLi href="/" active={true}>Home</NavLi>
|
||||
<NavLi href="/user">User</NavLi>
|
||||
<!-- <NavLi href="/services">Services</NavLi>
|
||||
<NavLi href="/pricing">Pricing</NavLi>
|
||||
<NavLi href="/contact">Contact</NavLi> -->
|
||||
</NavUl>
|
||||
</Navbar>
|
@ -28,6 +28,7 @@ body {
|
||||
|
||||
.group {
|
||||
position: relative;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 24px;
|
||||
min-height: 45px;
|
||||
}
|
||||
@ -212,6 +213,11 @@ body {
|
||||
transition: width 0.2s ease-out, padding-top 0.2s ease-out;
|
||||
}
|
||||
|
||||
.btn-wide {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loader_box {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
@ -2,22 +2,38 @@ import { Client } from "@hibas123/openauth-internalapi";
|
||||
import request, { RequestError } from "./request";
|
||||
|
||||
const provider = new Client.ServiceProvider((data) => {
|
||||
request("/api/jrpc", {}, "POST", data, true, true).then(result => {
|
||||
provider.onPacket(result);
|
||||
fetch("/api/jrpc", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then(res => {
|
||||
if (res.ok) return res.json();
|
||||
else throw new Error(res.statusText);
|
||||
}).then(res => {
|
||||
provider.onPacket(res);
|
||||
}).catch(err => {
|
||||
if (err instanceof RequestError) {
|
||||
let data = err.response;
|
||||
if (data.error && Array.isArray(data.error)) {
|
||||
data.error = data.error[0];
|
||||
}
|
||||
provider.onPacket(data);
|
||||
}
|
||||
});
|
||||
provider.onPacket({
|
||||
jsonrpc: "2.0",
|
||||
method: data.method,
|
||||
id: data.id,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: err.message,
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const InternalAPI = {
|
||||
Account: new Client.AccountService(provider),
|
||||
Security: new Client.SecurityService(provider),
|
||||
TwoFactor: new Client.TFAService(provider),
|
||||
Login: new Client.LoginService(provider),
|
||||
}
|
||||
|
||||
export default InternalAPI;
|
||||
|
||||
(window as any).InternalAPI = InternalAPI;
|
||||
|
@ -15,17 +15,6 @@
|
||||
</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>
|
||||
|
||||
<style>
|
||||
|
@ -1,124 +1,34 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {} from "flowbite-svelte";
|
||||
|
||||
import { LoginState } from "@hibas123/openauth-internalapi";
|
||||
import Theme from "../../components/theme";
|
||||
import loginState from "./state";
|
||||
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";
|
||||
import { onMount } from "svelte";
|
||||
import Username from "./Username.svelte";
|
||||
import Password from "./Password.svelte";
|
||||
import Success from "./Success.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();
|
||||
}
|
||||
const { state } = loginState;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Theme>
|
||||
<HoveringContentBox title="Login" {loading}>
|
||||
<HoveringContentBox title="Login" loading={$state.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 $state.success}
|
||||
<Success />
|
||||
{:else if !$state.username}
|
||||
<Username on:username={(evt) => loginState.setUsername(evt.detail)} />
|
||||
{:else if !$state.password}
|
||||
<Password
|
||||
username={$state.username}
|
||||
on:password={(evt) => loginState.setPassword(evt.detail)}
|
||||
/>
|
||||
{:else if $state.requireTwoFactor.length > 0}
|
||||
<TwoFactor />
|
||||
{/if}
|
||||
</form>
|
||||
</HoveringContentBox>
|
||||
<footer>
|
||||
<p>Powered by {appname}</p>
|
||||
</footer>
|
||||
</Theme>
|
||||
|
@ -1,84 +0,0 @@
|
||||
<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>
|
16
Frontend/src/pages/login/Error.svelte
Normal file
16
Frontend/src/pages/login/Error.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import loginState from "./state";
|
||||
|
||||
let { state } = loginState;
|
||||
</script>
|
||||
|
||||
{#if $state.error}
|
||||
<div class="error">{$state.error}</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
30
Frontend/src/pages/login/Password.svelte
Normal file
30
Frontend/src/pages/login/Password.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Error from "./Error.svelte";
|
||||
|
||||
let password: string = "";
|
||||
export let username: string;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<h3>Enter the password for {username}</h3>
|
||||
<div class="floating group">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="password"
|
||||
autofocus
|
||||
bind:value={password}
|
||||
/>
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="password">Password</label>
|
||||
<Error />
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-wide"
|
||||
on:click={() => dispatch("password", password)}>Next</button
|
||||
>
|
@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import Cleave from "cleave.js";
|
||||
import { onMount } from "svelte";
|
||||
import Error from "../Error.svelte";
|
||||
|
||||
export let error;
|
||||
// export let label;
|
||||
export let value;
|
||||
export let length = 6;
|
||||
@ -17,17 +17,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="floating group">
|
||||
<input id="noasidhglk" bind:this={input} autofocus bind:value />
|
||||
<input id="code-input" bind:this={input} autofocus bind:value />
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="noasidhglk">Code</label>
|
||||
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
||||
<label for="code-input">Code</label>
|
||||
|
||||
<Error />
|
||||
</div>
|
21
Frontend/src/pages/login/TF/TOTP.svelte
Normal file
21
Frontend/src/pages/login/TF/TOTP.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Error from "../Error.svelte";
|
||||
import loginState from "../state";
|
||||
import CodeInput from "./CodeInput.svelte";
|
||||
|
||||
export let id: string;
|
||||
export let name: string;
|
||||
|
||||
let code: string = "";
|
||||
|
||||
function send() {
|
||||
loginState.useTOTP(id, code);
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3>TOTP {name}</h3>
|
||||
<CodeInput bind:value={code} length={6} />
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary btn-wide" on:click={send}> Send </button>
|
||||
</div>
|
28
Frontend/src/pages/login/TF/WebAuthn.svelte
Normal file
28
Frontend/src/pages/login/TF/WebAuthn.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Error from "../Error.svelte";
|
||||
import loginState from "../state";
|
||||
import { startAuthentication } from "@simplewebauthn/browser";
|
||||
|
||||
export let id: string;
|
||||
|
||||
async function doAuth() {
|
||||
let challenge = await loginState.getWebAuthnChallenge(id);
|
||||
try {
|
||||
loginState.setLoading(true);
|
||||
let result = await startAuthentication(JSON.parse(challenge));
|
||||
await loginState.useWebAuthn(id, result);
|
||||
} catch (e) {
|
||||
loginState.setError(e.message);
|
||||
return;
|
||||
} finally {
|
||||
loginState.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
doAuth();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Error />
|
114
Frontend/src/pages/login/TwoFactor.svelte
Normal file
114
Frontend/src/pages/login/TwoFactor.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import loginState from "./state";
|
||||
import Icon from "./icons/Icon.svelte";
|
||||
import { TFAType } from "@hibas123/openauth-internalapi";
|
||||
import { onMount } from "svelte";
|
||||
import Totp from "./TF/TOTP.svelte";
|
||||
import Error from "./Error.svelte";
|
||||
import WebAuthn from "./TF/WebAuthn.svelte";
|
||||
|
||||
let { state } = loginState;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let selected = undefined;
|
||||
|
||||
$: {
|
||||
if ($state.requireTwoFactor?.length == 1) {
|
||||
selected = $state.requireTwoFactor[0];
|
||||
}
|
||||
}
|
||||
|
||||
const typeIconMap = {
|
||||
[TFAType.TOTP]: "Authenticator",
|
||||
[TFAType.BACKUP_CODE]: "BackupCode",
|
||||
[TFAType.WEBAUTHN]: "SecurityKey",
|
||||
[TFAType.APP_ALLOW]: "AppPush",
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !selected}
|
||||
<h3>Choose your 2FA method</h3>
|
||||
<ul>
|
||||
{#each $state.requireTwoFactor ?? [] as method}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li on:click={() => (selected = method)}>
|
||||
<div class="icon">
|
||||
<Icon icon_name={typeIconMap[method.tfatype]} />
|
||||
</div>
|
||||
|
||||
<div class="name">{method.name}</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
<Error />
|
||||
</ul>
|
||||
{:else}
|
||||
{#if selected.tfatype == TFAType.TOTP}
|
||||
<Totp id={selected.id} name={selected.name} />
|
||||
{:else if selected.tfatype == TFAType.BACKUP_CODE}
|
||||
backup
|
||||
{:else if selected.tfatype == TFAType.WEBAUTHN}
|
||||
<WebAuthn id={selected.id} />
|
||||
{:else if selected.tfatype == TFAType.APP_ALLOW}
|
||||
appallow
|
||||
{:else}
|
||||
<p>Unknown 2FA type</p>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
<a
|
||||
class="to-list"
|
||||
href="# "
|
||||
on:click={(evt) => {
|
||||
evt.preventDefault();
|
||||
loginState.setError(undefined);
|
||||
selected = undefined;
|
||||
}}
|
||||
>
|
||||
Choose another Method
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-inline-start: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
border-top: 1px grey solid;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 1rem;
|
||||
line-height: 1.5rem;
|
||||
font-size: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.to-list {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
@ -1,104 +0,0 @@
|
||||
<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>
|
29
Frontend/src/pages/login/Username.svelte
Normal file
29
Frontend/src/pages/login/Username.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Error from "./Error.svelte";
|
||||
|
||||
let username: string = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<h3>Enter your Username or your E-Mail Address</h3>
|
||||
<div class="floating group">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
bind:value={username}
|
||||
/>
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="username">Username or E-Mail</label>
|
||||
<Error />
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-wide"
|
||||
on:click={() => dispatch("username", username)}>Next</button
|
||||
>
|
@ -1,182 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
183
Frontend/src/pages/login/state.ts
Normal file
183
Frontend/src/pages/login/state.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import type { LoginState } from "@hibas123/openauth-internalapi";
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import InternalAPI from "../../helper/api";
|
||||
import sha from "../../helper/sha512";
|
||||
|
||||
interface LocalLoginState extends LoginState {
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
class LoginStore {
|
||||
state = writable<LocalLoginState>({
|
||||
username: undefined,
|
||||
password: false,
|
||||
passwordSalt: undefined,
|
||||
requireTwoFactor: [],
|
||||
success: false,
|
||||
loading: true,
|
||||
error: undefined
|
||||
})
|
||||
|
||||
isFinished = derived(this.state, $state => $state.success);
|
||||
|
||||
constructor() {
|
||||
this.state.subscribe((state) => {
|
||||
if (state.success) {
|
||||
setTimeout(() => {
|
||||
this.finish();
|
||||
}, 2000);
|
||||
}
|
||||
})
|
||||
this.getState();
|
||||
}
|
||||
|
||||
setLoading(loading: boolean) {
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
loading,
|
||||
error: loading ? undefined : current.error,
|
||||
}));
|
||||
}
|
||||
|
||||
setError(error: string) {
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
error,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
async getState() {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.GetState();
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async setUsername(username: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.Start(username);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
username
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async setPassword(password: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
const date = new Date().valueOf();
|
||||
let salt = get(this.state).passwordSalt
|
||||
let pw = sha(sha(salt + password) + date.toString());
|
||||
|
||||
let state = await InternalAPI.Login.UsePassword(pw, date);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async useTOTP(id: string, code: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.UseTOTP(id, code);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async useBackupCode(id: string, code: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.UseBackupCode(id, code);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async getWebAuthnChallenge(id: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let challenge = await InternalAPI.Login.GetWebAuthnChallenge(id);
|
||||
return challenge;
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async useWebAuthn(id: string, response: any) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.UseWebAuthn(id, JSON.stringify(response));
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async finish() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const loginState = new LoginStore();
|
||||
|
||||
export default loginState;
|
@ -1,50 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
import Api from "../api.ts";
|
||||
import CodeInput from "./codeInput.svelte";
|
||||
|
||||
let error = "";
|
||||
let code = "";
|
||||
export let finish;
|
||||
export let id;
|
||||
|
||||
export let otc = false;
|
||||
let title = otc ? "One Time Code (OTC)" : "Backup Code";
|
||||
let length = otc ? 6 : 8;
|
||||
|
||||
async function sendCode() {
|
||||
let c = code.replace(/\s+/g, "");
|
||||
if (c.length < length) {
|
||||
error = `Code must be ${length} digits long!`;
|
||||
} else {
|
||||
error = "";
|
||||
let res;
|
||||
if (otc) res = await Api.sendOTC(id, c);
|
||||
else res = await Api.sendBackup(id, c);
|
||||
if (res.error) error = res.error;
|
||||
else finish(true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-next {
|
||||
margin: 0;
|
||||
margin-left: auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>{title}</h3>
|
||||
|
||||
<CodeInput bind:value={code} label="Code" {error} {length} />
|
||||
|
||||
<div class="actions">
|
||||
<ToList {finish} />
|
||||
<button class="btn btn-primary btn-next" on:click={sendCode}> Send </button>
|
||||
</div>
|
@ -1,389 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
|
||||
let error = "";
|
||||
let code = "";
|
||||
export let device = "Handy01";
|
||||
// export let deviceId = "";
|
||||
|
||||
export let finish;
|
||||
|
||||
async function requestPush() {
|
||||
// Push Request
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.windows8 {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.windows8 .wBall {
|
||||
position: absolute;
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
opacity: 0;
|
||||
transform: rotate(225deg);
|
||||
-o-transform: rotate(225deg);
|
||||
-ms-transform: rotate(225deg);
|
||||
-webkit-transform: rotate(225deg);
|
||||
-moz-transform: rotate(225deg);
|
||||
animation: orbit 5.7425s infinite;
|
||||
-o-animation: orbit 5.7425s infinite;
|
||||
-ms-animation: orbit 5.7425s infinite;
|
||||
-webkit-animation: orbit 5.7425s infinite;
|
||||
-moz-animation: orbit 5.7425s infinite;
|
||||
}
|
||||
|
||||
.windows8 .wBall .wInnerBall {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: rgb(0, 140, 255);
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.windows8 #wBall_1 {
|
||||
animation-delay: 1.256s;
|
||||
-o-animation-delay: 1.256s;
|
||||
-ms-animation-delay: 1.256s;
|
||||
-webkit-animation-delay: 1.256s;
|
||||
-moz-animation-delay: 1.256s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_2 {
|
||||
animation-delay: 0.243s;
|
||||
-o-animation-delay: 0.243s;
|
||||
-ms-animation-delay: 0.243s;
|
||||
-webkit-animation-delay: 0.243s;
|
||||
-moz-animation-delay: 0.243s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_3 {
|
||||
animation-delay: 0.5065s;
|
||||
-o-animation-delay: 0.5065s;
|
||||
-ms-animation-delay: 0.5065s;
|
||||
-webkit-animation-delay: 0.5065s;
|
||||
-moz-animation-delay: 0.5065s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_4 {
|
||||
animation-delay: 0.7495s;
|
||||
-o-animation-delay: 0.7495s;
|
||||
-ms-animation-delay: 0.7495s;
|
||||
-webkit-animation-delay: 0.7495s;
|
||||
-moz-animation-delay: 0.7495s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_5 {
|
||||
animation-delay: 1.003s;
|
||||
-o-animation-delay: 1.003s;
|
||||
-ms-animation-delay: 1.003s;
|
||||
-webkit-animation-delay: 1.003s;
|
||||
-moz-animation-delay: 1.003s;
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
transform: rotate(180deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
transform: rotate(300deg);
|
||||
animation-timing-function: linear;
|
||||
origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(410deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
transform: rotate(645deg);
|
||||
animation-timing-function: linear;
|
||||
origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: rotate(770deg);
|
||||
animation-timing-function: ease-out;
|
||||
origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
transform: rotate(900deg);
|
||||
animation-timing-function: ease-out;
|
||||
origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-o-transform: rotate(180deg);
|
||||
-o-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(300deg);
|
||||
-o-animation-timing-function: linear;
|
||||
-o-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(410deg);
|
||||
-o-animation-timing-function: ease-in-out;
|
||||
-o-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(645deg);
|
||||
-o-animation-timing-function: linear;
|
||||
-o-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(770deg);
|
||||
-o-animation-timing-function: ease-out;
|
||||
-o-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(900deg);
|
||||
-o-animation-timing-function: ease-out;
|
||||
-o-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-o-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-o-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-ms-transform: rotate(180deg);
|
||||
-ms-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(300deg);
|
||||
-ms-animation-timing-function: linear;
|
||||
-ms-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(410deg);
|
||||
-ms-animation-timing-function: ease-in-out;
|
||||
-ms-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(645deg);
|
||||
-ms-animation-timing-function: linear;
|
||||
-ms-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(770deg);
|
||||
-ms-animation-timing-function: ease-out;
|
||||
-ms-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(900deg);
|
||||
-ms-animation-timing-function: ease-out;
|
||||
-ms-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-ms-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-ms-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-webkit-transform: rotate(180deg);
|
||||
-webkit-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(300deg);
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(410deg);
|
||||
-webkit-animation-timing-function: ease-in-out;
|
||||
-webkit-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(645deg);
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(770deg);
|
||||
-webkit-animation-timing-function: ease-out;
|
||||
-webkit-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(900deg);
|
||||
-webkit-animation-timing-function: ease-out;
|
||||
-webkit-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-moz-transform: rotate(180deg);
|
||||
-moz-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(300deg);
|
||||
-moz-animation-timing-function: linear;
|
||||
-moz-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(410deg);
|
||||
-moz-animation-timing-function: ease-in-out;
|
||||
-moz-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(645deg);
|
||||
-moz-animation-timing-function: linear;
|
||||
-moz-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(770deg);
|
||||
-moz-animation-timing-function: ease-out;
|
||||
-moz-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(900deg);
|
||||
-moz-animation-timing-function: ease-out;
|
||||
-moz-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-moz-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-moz-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>SMS</h3>
|
||||
|
||||
<p>A code was sent to your Device <b>{device}</b></p>
|
||||
|
||||
<div class="windows8">
|
||||
<div class="wBall" id="wBall_1">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_2">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_3">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_4">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_5">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error">{error}</div>
|
||||
<ToList {finish} />
|
@ -1,49 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
|
||||
const states = {
|
||||
approve: 1,
|
||||
enter: 2,
|
||||
};
|
||||
let state = states.approve;
|
||||
|
||||
let error = "";
|
||||
let code = "";
|
||||
export let number = "+4915...320";
|
||||
//export let finish;
|
||||
|
||||
function validateCode() {}
|
||||
|
||||
function sendCode() {
|
||||
// Send request to Server
|
||||
state = states.enter;
|
||||
//finish()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--error: red;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>SMS</h3>
|
||||
{#if state === states.approve}
|
||||
<p>Send SMS to {number}</p>
|
||||
<button class="btn btn-primary" on:click={sendCode}>Send</button>
|
||||
{:else}
|
||||
<p>A code was sent to you. Please enter</p>
|
||||
<input type="number" placeholder="Code" bind:value={code} />
|
||||
<button class="btn btn-primary" on:click={validateCode}>Send</button>
|
||||
<br />
|
||||
<a href="# " on:click|preventDefault={() => (state = states.approve)}>
|
||||
Not received?
|
||||
</a>
|
||||
{/if}
|
||||
<div class="error">{error}</div>
|
||||
|
||||
<ToList {finish} />
|
@ -1,17 +0,0 @@
|
||||
<script>
|
||||
export let finish = () => {};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>
|
||||
<a href="# " on:click={evt => evt.preventDefault() || finish(false)}>
|
||||
Choose another Method
|
||||
</a>
|
||||
</p>
|
@ -1,69 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
|
||||
export let finish;
|
||||
|
||||
const states = {
|
||||
getChallenge: 0,
|
||||
requestUser: 1,
|
||||
sendChallenge: 2,
|
||||
error: 3
|
||||
};
|
||||
|
||||
let state = states.getChallenge;
|
||||
|
||||
let error = "";
|
||||
|
||||
const onError = err => {
|
||||
state = states.error;
|
||||
error = err.message;
|
||||
};
|
||||
|
||||
let challenge;
|
||||
|
||||
async function requestUser() {
|
||||
state = states.requestUser;
|
||||
let res = await window.navigator.credentials.get({
|
||||
publicKey: challenge
|
||||
});
|
||||
state = states.sendChallenge();
|
||||
let r = res.response;
|
||||
let data = encode({
|
||||
authenticatorData: r.authenticatorData,
|
||||
clientDataJSON: r.clientDataJSON,
|
||||
signature: r.signature,
|
||||
userHandle: r.userHandle
|
||||
});
|
||||
let { success } = fetch("https://localhost:8444/auth", {
|
||||
body: data,
|
||||
method: "POST"
|
||||
}).then(res => res.json());
|
||||
if (success) {
|
||||
finish(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function getChallenge() {
|
||||
state = states.getChallenge;
|
||||
challenge = await fetch("https://localhost:8444/auth")
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(data => decode(MessagePack.Buffer.from(data)));
|
||||
|
||||
requestUser().catch(onError);
|
||||
}
|
||||
getChallenge().catch(onError);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--error: red;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>U2F Security Key</h3>
|
||||
<h4>This Method is currently not supported. Please choose another one!</h4>
|
||||
<ToList {finish} />
|
@ -84,9 +84,25 @@ async function onMessage(msg: MessageEvent<any>) {
|
||||
const url = new URL(msg.origin);
|
||||
setAppName(url.hostname);
|
||||
|
||||
if (!msg.data.client_id) {
|
||||
alert("The site requesting the login is not valid");
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!msg.data.type || msg.data.type === "jwt") {
|
||||
console.log("JWT Request");
|
||||
|
||||
await request(
|
||||
"/api/user/oauth/permissions",
|
||||
{
|
||||
client_id: msg.data.client_id,
|
||||
origin: url.hostname,
|
||||
permissions: permissions.join(","),
|
||||
}
|
||||
); // Will fail if client does not exist
|
||||
|
||||
await new Promise<void>((yes) => {
|
||||
console.log("Await user acceptance");
|
||||
setLoading(false);
|
||||
|
@ -1,13 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import MainNavbar from "../../components/MainNavbar.svelte";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import { CurrentPage } from "./nav";
|
||||
import PersonalInfo from "./pages/PersonalInfo.svelte";
|
||||
import Security from "./pages/Security.svelte";
|
||||
|
||||
let sidebarOpen = false;
|
||||
let sidebarOpenVisible = false;
|
||||
|
||||
onMount(() => {
|
||||
const unsub = CurrentPage.subscribe(() => {
|
||||
sidebarOpen = false;
|
||||
});
|
||||
|
||||
return unsub;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grid main-grid min-h-screen overflow-hidden">
|
||||
<div class="col-span-2">
|
||||
<MainNavbar bind:sidebarOpen bind:sidebarOpenVisible />
|
||||
</div>
|
||||
<div>
|
||||
<Sidebar />
|
||||
<Sidebar bind:sidebarOpen bind:sidebarOpenVisible />
|
||||
</div>
|
||||
<div class="overflow-auto p-4">
|
||||
{#if $CurrentPage == "personal-info"}
|
||||
@ -21,5 +37,6 @@
|
||||
<style>
|
||||
.main-grid {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,197 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ContactInfo,
|
||||
type Account,
|
||||
Gender,
|
||||
} from "@hibas123/openauth-internalapi";
|
||||
import InternalAPI from "../../../helper/api";
|
||||
import Loading from "../Loading.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
Heading,
|
||||
Spinner,
|
||||
Helper,
|
||||
} from "flowbite-svelte";
|
||||
|
||||
let profileInfo: Account;
|
||||
let loadedProfileInfo: Account;
|
||||
let contactInfo: ContactInfo;
|
||||
|
||||
let loading = true;
|
||||
let error: string | undefined;
|
||||
|
||||
async function load() {
|
||||
error = undefined;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
profileInfo = await InternalAPI.Account.GetProfile();
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
contactInfo = await InternalAPI.Account.GetContactInfos();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
let savingProfile = false;
|
||||
|
||||
async function saveProfileChanges() {
|
||||
savingProfile = true;
|
||||
|
||||
try {
|
||||
await new Promise((yes) => setTimeout(yes, 1000));
|
||||
await InternalAPI.Account.UpdateProfile(profileInfo);
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
savingProfile = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: hasProfileChanged =
|
||||
JSON.stringify(profileInfo) != JSON.stringify(loadedProfileInfo);
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
let genders = [
|
||||
{
|
||||
value: Gender.None,
|
||||
name: "Not saying",
|
||||
},
|
||||
{
|
||||
value: Gender.Male,
|
||||
name: "Male",
|
||||
},
|
||||
{
|
||||
value: Gender.Female,
|
||||
name: "Female",
|
||||
},
|
||||
{
|
||||
value: Gender.Other,
|
||||
name: "Other",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Loading {loading} {error}>
|
||||
<Card>
|
||||
<Heading tag="h5">General Account Details</Heading>
|
||||
<hr class="mb-6" />
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={!hasProfileChanged || savingProfile}
|
||||
on:click={saveProfileChanges}
|
||||
>
|
||||
{#if savingProfile}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Saving...
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mt-4">
|
||||
<Heading tag="h5">Contact Details (WIP)</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
<Heading tag="h6" color="gray">Mails</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.mail as mail}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="mail-input" class="block mb-2">Mail</Label> -->
|
||||
<Input
|
||||
id="mail-input"
|
||||
placeholder="Mail"
|
||||
bind:value={mail.mail}
|
||||
color={mail.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if mail.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> E-Mail is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> E-Mail needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Heading tag="h6" color="gray">Phones</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.phone as phone}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="phone-input" class="block mb-2">Phone</Label> -->
|
||||
<Input
|
||||
id="phone-input"
|
||||
placeholder="Phone"
|
||||
bind:value={phone.phone}
|
||||
color={phone.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if phone.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> Phone is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> Phone needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- <div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div> -->
|
||||
|
||||
<!-- <Button>Save</Button> -->
|
||||
</Card>
|
||||
</Loading>
|
@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarGroup,
|
||||
@ -6,9 +6,29 @@
|
||||
SidebarWrapper,
|
||||
} from "flowbite-svelte";
|
||||
import { CurrentPage } from "./nav";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let sidebarOpen = false;
|
||||
export let sidebarOpenVisible = false;
|
||||
|
||||
$: open = !sidebarOpenVisible || sidebarOpen;
|
||||
|
||||
onMount(() => {
|
||||
const mq = window.matchMedia("(max-width: 768px)");
|
||||
const onChange = (e: MediaQueryListEvent) => {
|
||||
sidebarOpenVisible = e.matches;
|
||||
};
|
||||
mq.addEventListener("change", onChange);
|
||||
|
||||
onChange({ matches: mq.matches } as MediaQueryListEvent);
|
||||
|
||||
return () => {
|
||||
mq.removeEventListener("change", onChange);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<Sidebar class="h-screen">
|
||||
<Sidebar class="h-screen" style={open ? "display: block" : "display: none"}>
|
||||
<SidebarWrapper class="h-full">
|
||||
<SidebarGroup>
|
||||
<SidebarItem
|
||||
|
36
Frontend/src/pages/user/pages/AddTwoFactor.svelte
Normal file
36
Frontend/src/pages/user/pages/AddTwoFactor.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Listgroup, ListgroupItem, Modal, Radio } from "flowbite-svelte";
|
||||
import Totp from "./TwoFactorRegistration/TOTP.svelte";
|
||||
import WebAuthn from "./TwoFactorRegistration/WebAuthn.svelte";
|
||||
|
||||
export let open: boolean;
|
||||
|
||||
let selectedType = undefined;
|
||||
$: {
|
||||
if (!open) {
|
||||
selectedType = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:open size="md" autoclose={false} class="w-full">
|
||||
{#if !selectedType}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select type
|
||||
</h3>
|
||||
<Listgroup active class="w-full">
|
||||
<ListgroupItem
|
||||
class="gap-2 px-4 py-4"
|
||||
on:click={() => (selectedType = "totp")}>TOTP</ListgroupItem
|
||||
>
|
||||
<ListgroupItem
|
||||
class="gap-2 px-4 py-4"
|
||||
on:click={() => (selectedType = "webauthn")}>WebAuthn</ListgroupItem
|
||||
>
|
||||
</Listgroup>
|
||||
{:else if selectedType == "totp"}
|
||||
<Totp on:reload />
|
||||
{:else if selectedType == "webauthn"}
|
||||
<WebAuthn on:reload />
|
||||
{/if}
|
||||
</Modal>
|
203
Frontend/src/pages/user/pages/PersonalInfo.svelte
Normal file
203
Frontend/src/pages/user/pages/PersonalInfo.svelte
Normal file
@ -0,0 +1,203 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ContactInfo,
|
||||
type Profile,
|
||||
Gender,
|
||||
} from "@hibas123/openauth-internalapi";
|
||||
import InternalAPI from "../../../helper/api";
|
||||
import Loading from "../Loading.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
Heading,
|
||||
Spinner,
|
||||
Helper,
|
||||
} from "flowbite-svelte";
|
||||
|
||||
let profileInfo: Profile;
|
||||
let loadedProfileInfo: Profile;
|
||||
let contactInfo: ContactInfo;
|
||||
|
||||
let loading = true;
|
||||
let error: string | undefined;
|
||||
|
||||
async function load() {
|
||||
error = undefined;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
profileInfo = await InternalAPI.Account.GetProfile();
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
contactInfo = await InternalAPI.Account.GetContactInfos();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
let savingProfile = false;
|
||||
|
||||
async function saveProfileChanges() {
|
||||
savingProfile = true;
|
||||
|
||||
try {
|
||||
await new Promise((yes) => setTimeout(yes, 1000));
|
||||
await InternalAPI.Account.UpdateProfile(profileInfo);
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
savingProfile = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: hasProfileChanged =
|
||||
JSON.stringify(profileInfo) != JSON.stringify(loadedProfileInfo);
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
let genders = [
|
||||
{
|
||||
value: Gender.None,
|
||||
name: "Not saying",
|
||||
},
|
||||
{
|
||||
value: Gender.Male,
|
||||
name: "Male",
|
||||
},
|
||||
{
|
||||
value: Gender.Female,
|
||||
name: "Female",
|
||||
},
|
||||
{
|
||||
value: Gender.Other,
|
||||
name: "Other",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Loading {loading} {error}>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<Card size="md" class="w-full">
|
||||
<Heading tag="h5">General Account Details</Heading>
|
||||
<hr class="mb-6" />
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input
|
||||
id="name-input"
|
||||
placeholder="Name"
|
||||
bind:value={profileInfo.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={!hasProfileChanged || savingProfile}
|
||||
on:click={saveProfileChanges}
|
||||
>
|
||||
{#if savingProfile}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Saving...
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card size="md" class="w-full">
|
||||
<Heading tag="h5">Contact Details (WIP)</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
<Heading tag="h6" color="gray">Mails</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.mail as mail}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="mail-input" class="block mb-2">Mail</Label> -->
|
||||
<Input
|
||||
id="mail-input"
|
||||
placeholder="Mail"
|
||||
bind:value={mail.mail}
|
||||
color={mail.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if mail.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> E-Mail is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> E-Mail needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Heading tag="h6" color="gray">Phones</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.phone as phone}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="phone-input" class="block mb-2">Phone</Label> -->
|
||||
<Input
|
||||
id="phone-input"
|
||||
placeholder="Phone"
|
||||
bind:value={phone.phone}
|
||||
color={phone.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if phone.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> Phone is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> Phone needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- <div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div> -->
|
||||
|
||||
<!-- <Button>Save</Button> -->
|
||||
</Card>
|
||||
</div>
|
||||
</Loading>
|
@ -1,12 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ContactInfo,
|
||||
type Account,
|
||||
Gender,
|
||||
Token,
|
||||
TwoFactor,
|
||||
TFAType,
|
||||
} from "@hibas123/openauth-internalapi";
|
||||
import { Session, TFAOption, TFAType } from "@hibas123/openauth-internalapi";
|
||||
import InternalAPI from "../../../helper/api";
|
||||
import Loading from "../Loading.svelte";
|
||||
import { onMount } from "svelte";
|
||||
@ -27,18 +20,20 @@
|
||||
TableBodyCell,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
Alert,
|
||||
} from "flowbite-svelte";
|
||||
import AddTwoFactor from "./AddTwoFactor.svelte";
|
||||
|
||||
let tokens: Token[];
|
||||
let twofactors: TwoFactor[];
|
||||
let tokens: Session[];
|
||||
let twofactors: TFAOption[];
|
||||
let error: string | undefined;
|
||||
let loading = true;
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
try {
|
||||
tokens = await InternalAPI.Security.GetTokens();
|
||||
twofactors = await InternalAPI.Security.GetTwofactorOptions();
|
||||
tokens = await InternalAPI.Security.GetSessions();
|
||||
twofactors = await InternalAPI.TwoFactor.GetOptions();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
@ -46,13 +41,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
try {
|
||||
tokens = await InternalAPI.Security.GetSessions();
|
||||
twofactors = await InternalAPI.TwoFactor.GetOptions();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
async function revokeToken(id: string) {
|
||||
try {
|
||||
await InternalAPI.Security.RevokeToken(id);
|
||||
await InternalAPI.Security.RevokeSession(id);
|
||||
await load();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
@ -65,6 +69,49 @@
|
||||
[TFAType.BACKUP_CODE]: "Backup-Code",
|
||||
[TFAType.APP_ALLOW]: "App-Auth",
|
||||
};
|
||||
|
||||
let addTwoFactorOpen = false;
|
||||
|
||||
function openAddTwoFactor() {
|
||||
addTwoFactorOpen = true;
|
||||
}
|
||||
|
||||
async function deleteTwoFactor(id: string) {
|
||||
try {
|
||||
await InternalAPI.TwoFactor.Delete(id);
|
||||
await reload();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
let old_pw = "";
|
||||
let new_pw = "";
|
||||
let new_pw_repeat = "";
|
||||
|
||||
let change_password_success = false;
|
||||
let change_password_error: string | undefined;
|
||||
|
||||
function changePassword() {
|
||||
change_password_success = false;
|
||||
change_password_error = undefined;
|
||||
if (new_pw !== new_pw_repeat) {
|
||||
change_password_error = "Passwords do not match";
|
||||
return;
|
||||
}
|
||||
|
||||
InternalAPI.Security.ChangePassword(old_pw, new_pw)
|
||||
.then(() => {
|
||||
change_password_error = undefined;
|
||||
old_pw = "";
|
||||
new_pw = "";
|
||||
new_pw_repeat = "";
|
||||
change_password_success = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
change_password_error = e.message;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Loading {loading} {error}>
|
||||
@ -107,19 +154,31 @@
|
||||
<Heading tag="h5">Change Password</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#if change_password_success}
|
||||
<Alert color="green">Password changed successfully.</Alert>
|
||||
{/if}
|
||||
|
||||
{#if change_password_error}
|
||||
<Alert color="red">{change_password_error}</Alert>
|
||||
{/if}
|
||||
|
||||
<div class="mb-6">
|
||||
<Label for="oldPassword">Old Password</Label>
|
||||
<Input type="password" id="oldPassword" />
|
||||
<Input type="password" id="oldPassword" bind:value={old_pw} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="newPassword">New Password</Label>
|
||||
<Input type="password" id="newPassword" />
|
||||
<Input type="password" id="newPassword" bind:value={new_pw} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="newPasswordRepeat">Repeat New Password</Label>
|
||||
<Input type="password" id="newPasswordRepeat" />
|
||||
<Input
|
||||
type="password"
|
||||
id="newPasswordRepeat"
|
||||
bind:value={new_pw_repeat}
|
||||
/>
|
||||
</div>
|
||||
<Button class="mt-4">Change Password</Button>
|
||||
<Button class="mt-4" on:click={changePassword}>Change Password</Button>
|
||||
</Card>
|
||||
|
||||
<Card size="xl" class="mt-4">
|
||||
@ -130,12 +189,21 @@
|
||||
{#each twofactors as tfa}
|
||||
<AccordionItem>
|
||||
<span slot="header">{tfa.name ?? typeToName[tfa.tfatype]}</span>
|
||||
<div>
|
||||
<Button
|
||||
color="red"
|
||||
class="mt-4"
|
||||
on:click={() => deleteTwoFactor(tfa.id)}>Delete</Button
|
||||
>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
{/each}
|
||||
</Accordion>
|
||||
<Button class="mt-4">Add Option</Button>
|
||||
<Button class="mt-4" on:click={openAddTwoFactor}>Add Option</Button>
|
||||
</Card>
|
||||
|
||||
<AddTwoFactor on:reload={reload} bind:open={addTwoFactorOpen} />
|
||||
|
||||
<!-- <Card size="xl" class="mt-4">
|
||||
<Heading tag="h5">Delete Account</Heading>
|
||||
<hr class="mb-6" />
|
102
Frontend/src/pages/user/pages/TwoFactorRegistration/TOTP.svelte
Normal file
102
Frontend/src/pages/user/pages/TwoFactorRegistration/TOTP.svelte
Normal file
@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Button, Input, Label, Spinner } from "flowbite-svelte";
|
||||
import InternalAPI from "../../../../helper/api";
|
||||
|
||||
import type { TFANewTOTP } from "@hibas123/openauth-internalapi";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let stage = "get-name";
|
||||
let name: string = "";
|
||||
let code: string = "";
|
||||
let totp: TFANewTOTP;
|
||||
|
||||
let creatingTOTP = false;
|
||||
let verifingTOTP = false;
|
||||
|
||||
async function createTOTP() {
|
||||
creatingTOTP = true;
|
||||
try {
|
||||
totp = await InternalAPI.TwoFactor.AddTOTP(name);
|
||||
stage = "verify";
|
||||
} catch (err) {
|
||||
} finally {
|
||||
creatingTOTP = false;
|
||||
}
|
||||
}
|
||||
|
||||
let verifyError = undefined;
|
||||
async function verifyTOTP() {
|
||||
verifingTOTP = true;
|
||||
verifyError = undefined;
|
||||
try {
|
||||
await InternalAPI.TwoFactor.VerifyTOTP(totp.id, code);
|
||||
stage = "done";
|
||||
dispatch("reload");
|
||||
} catch (err) {
|
||||
verifyError = err.message;
|
||||
code = "";
|
||||
} finally {
|
||||
verifingTOTP = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if stage == "get-name"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select a name
|
||||
</h3>
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={name} />
|
||||
</div>
|
||||
|
||||
<Button disabled={creatingTOTP} on:click={createTOTP}>
|
||||
{#if creatingTOTP}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Creating...
|
||||
{:else}
|
||||
Create
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if stage == "verify"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Save secret and verify
|
||||
</h3>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<img class="w-64" src={totp.qr} alt="Secret {totp.secret}" />
|
||||
<div>Manually:</div>
|
||||
<div class="text-sm">
|
||||
{totp.secret}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="code-input" class="block mb-2">Code</Label>
|
||||
<Input id="code-input" placeholder="Code" bind:value={code} />
|
||||
</div>
|
||||
|
||||
{#if verifyError}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">{verifyError}</p>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Button disabled={verifingTOTP} on:click={verifyTOTP}>
|
||||
{#if verifingTOTP}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Verify...
|
||||
{:else}
|
||||
Verify
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if stage == "done"}
|
||||
<Alert color="green">
|
||||
<h2 class="text-lg font-bold">Success</h2>
|
||||
<p class="mt-2">Your TOTP has been created.</p>
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">An unknown error occured.</p>
|
||||
</Alert>
|
||||
{/if}
|
@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Button, Input, Label, Spinner } from "flowbite-svelte";
|
||||
import { startRegistration } from "@simplewebauthn/browser";
|
||||
import InternalAPI from "../../../../helper/api";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let stage = "get-name";
|
||||
let name: string = "";
|
||||
|
||||
let creating = false;
|
||||
let error = undefined;
|
||||
|
||||
async function register() {
|
||||
creating = true;
|
||||
error = undefined;
|
||||
try {
|
||||
let challenge_data = await InternalAPI.TwoFactor.AddWebauthn(name);
|
||||
let challenge = JSON.parse(challenge_data.challenge);
|
||||
stage = "verify";
|
||||
|
||||
creating = false;
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
console.log(challenge);
|
||||
let response = await startRegistration(challenge);
|
||||
|
||||
await InternalAPI.TwoFactor.VerifyWebAuthn(
|
||||
challenge_data.id,
|
||||
JSON.stringify(response)
|
||||
);
|
||||
|
||||
stage = "done";
|
||||
dispatch("reload");
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
console.error(err);
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">An unknown error occured.</p>
|
||||
</Alert>
|
||||
{:else if stage == "get-name"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select a name
|
||||
</h3>
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={name} />
|
||||
</div>
|
||||
|
||||
<Button disabled={creating} on:click={register}>
|
||||
{#if creating}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Creating...
|
||||
{:else}
|
||||
Create
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if stage == "verify"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select device to add
|
||||
</h3>
|
||||
<!-- <div class="flex flex-col justify-center items-center">
|
||||
<img class="w-64" src={totp.qr} alt="Secret {totp.secret}" />
|
||||
<div>Manually:</div>
|
||||
<div class="text-sm">
|
||||
{totp.secret}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="code-input" class="block mb-2">Code</Label>
|
||||
<Input id="code-input" placeholder="Code" bind:value={code} />
|
||||
</div>
|
||||
|
||||
{#if verifyError}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">{verifyError}</p>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Button disabled={verifing} on:click={verifyTOTP}>
|
||||
{#if verifing}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Verify...
|
||||
{:else}
|
||||
Verify
|
||||
{/if}
|
||||
</Button> -->
|
||||
{:else if stage == "done"}
|
||||
<Alert color="green">
|
||||
<h2 class="text-lg font-bold">Success</h2>
|
||||
<p class="mt-2">Your WebAuthn device has been registered.</p>
|
||||
</Alert>
|
||||
{/if}
|
@ -1,207 +0,0 @@
|
||||
<script>
|
||||
import AccountPage from "./Pages/Account.svelte";
|
||||
import SecurityPage from "./Pages/Security.svelte";
|
||||
import { slide, fade } from "svelte/transition";
|
||||
|
||||
const pages = [
|
||||
{
|
||||
id: "account",
|
||||
title: "Account",
|
||||
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgdmlld0JveD0iMCAwIDUwMCA1MDAiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT4wMSBAZnVsbHdpZHRoPC90aXRsZT48ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz48ZyBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj48ZyBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDU3LjUsMjUwIEM0NTcuNSwxMzUuOTUzMTk5IDM2NS4wNDY4MDEsNDMuNSAyNTEsNDMuNSBDMTM2Ljk1MzE5OSw0My41IDQ0LjUsMTM1Ljk1MzE5OSA0NC41LDI1MCBDNDQuNSwzNjQuMDQ2ODAxIDEzNi45NTMxOTksNDU2LjUgMjUxLDQ1Ni41IEMzNjUuMDQ2ODAxLDQ1Ni41IDQ1Ny41LDM2NC4wNDY4MDEgNDU3LjUsMjUwIFogTTU3LjUsMjUwIEM1Ny41LDE0My4xMzI5MDEgMTQ0LjEzMjkwMSw1Ni41IDI1MSw1Ni41IEMzNTcuODY3MDk5LDU2LjUgNDQ0LjUsMTQzLjEzMjkwMSA0NDQuNSwyNTAgQzQ0NC41LDM1Ni44NjcwOTkgMzU3Ljg2NzA5OSw0NDMuNSAyNTEsNDQzLjUgQzE0NC4xMzI5MDEsNDQzLjUgNTcuNSwzNTYuODY3MDk5IDU3LjUsMjUwIFoiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD48cGF0aCBkPSJNMjUxLjUsMjUyLjkzMzk2MiBDMTk2Ljg1NDE5LDI1Mi45MzM5NjIgMTUyLjUsMjk2LjM2MDgwOSAxNTIuNSwzNTAgQzE1Mi41LDM1My41ODk4NTEgMTU1LjQxMDE0OSwzNTYuNSAxNTksMzU2LjUgTDM0NCwzNTYuNSBDMzQ3LjU4OTg1MSwzNTYuNSAzNTAuNSwzNTMuNTg5ODUxIDM1MC41LDM1MCBDMzUwLjUsMjk2LjM2MDgwOSAzMDYuMTQ1ODEsMjUyLjkzMzk2MiAyNTEuNSwyNTIuOTMzOTYyIFogTTE2NS41LDM0My41MDAwMDEgQzE2NS41LDMwMy42MDI3MDggMjAzLjk3MzEzMSwyNjUuOTMzOTYyIDI1MS41LDI2NS45MzM5NjIgQzI5OS4wMjY4NjksMjY1LjkzMzk2MiAzMzcuNSwzMDMuNjAyNzA4IDMzNy40OTk5OTcsMzQzLjUwMDAwMSBMMTY1LjUsMzQzLjUwMDAwMSBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PHBhdGggZD0iTTMwNC4yNSwxOTMuMzk2MjI2IEMzMDQuMjUsMTY1LjgwODIwMiAyODEuNDY1NDI0LDE0My41IDI1My40MjcwODMsMTQzLjUgQzIyNS4zODg3NDIsMTQzLjUgMjAyLjYwNDE2NywxNjUuODA4MjAyIDIwMi42MDQxNjcsMTkzLjM5NjIyNiBDMjAyLjYwNDE2NywyMjAuOTg0MjUgMjI1LjM4ODc0MiwyNDMuMjkyNDUzIDI1My40MjcwODMsMjQzLjI5MjQ1MyBDMjgxLjQ2NTQyNCwyNDMuMjkyNDUzIDMwNC4yNSwyMjAuOTg0MjUgMzA0LjI1LDE5My4zOTYyMjYgWiBNMjE1LjYwNDE2NywxOTMuMzk2MjI2IEMyMTUuNjA0MTY3LDE3My4wNTAxMDIgMjMyLjUwNzY4MywxNTYuNSAyNTMuNDI3MDgzLDE1Ni41IEMyNzQuMzQ2NDg0LDE1Ni41IDI5MS4yNSwxNzMuMDUwMTAyIDI5MS4yNSwxOTMuMzk2MjI2IEMyOTEuMjUsMjEzLjc0MjM1MSAyNzQuMzQ2NDg0LDIzMC4yOTI0NTMgMjUzLjQyNzA4MywyMzAuMjkyNDUzIEMyMzIuNTA3NjgzLDIzMC4yOTI0NTMgMjE1LjYwNDE2NywyMTMuNzQyMzUxIDIxNS42MDQxNjcsMTkzLjM5NjIyNiBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PC9nPjwvZz48L3N2Zz4=",
|
||||
component: AccountPage,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
title: "Security",
|
||||
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyIDUxMjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik00NDUuOCwzNy4zYy0xLjItMC43LTIuNy0wLjgtMy45LTAuMWMtNC4zLDIuMi0xMS42LDMuNC0yMS45LDMuNGMtMzMuNiwwLTkwLjgtMTIuNC0xMjguNy0yMC41Yy0xNC0zLTI2LjEtNS42LTM0LjEtNyAgIGMtMC40LTAuMS0wLjktMC4xLTEuNCwwYy03LjIsMS4yLTE4LjUsMy42LTMxLjcsNi4zQzE4NC4zLDI3LjYsMTI0LjIsNDAsOTAuNiw0MGMtMTEuNiwwLTE3LTEuNS0xOS41LTIuOCAgIGMtMS4yLTAuNi0yLjctMC42LTMuOSwwLjFzLTEuOSwyLTEuOSwzLjRjMCw3My4xLDMuOCwxNjguNCwzMy45LDI1Ny43YzE0LjMsNDIuOCwzMy41LDgwLjcsNTcuMSwxMTIuNyAgIGMyNi42LDM2LDU5LjcsNjUuNyw5OC4zLDg4LjNjMC42LDAuNCwxLjMsMC41LDIsMC41czEuNC0wLjIsMi0wLjVjMzguNi0yMi42LDcxLjYtNTIuMyw5OC4yLTg4LjNjMjMuNi0zMiw0Mi45LTY5LjksNTcuMS0xMTIuNyAgIGMyOS41LTg3LjYsMzMuNy0xNzkuNCwzMy45LTI1Ny43QzQ0Ny43LDM5LjQsNDQ3LDM4LjEsNDQ1LjgsMzcuM3ogTTQwNi4zLDI5NS45Yy0yOS4zLDg4LjEtNzkuNywxNTMuOC0xNDkuOCwxOTUuNCAgIEMxODYuNCw0NDkuNywxMzYsMzg0LDEwNi43LDI5NS45Qzc3LjgsMjEwLDczLjQsMTE4LjEsNzMuMiw0Ni40YzQuNSwxLjEsMTAuMiwxLjYsMTcuMywxLjZjMCwwLDAsMCwwLDAgICBjMzQuNSwwLDk1LjEtMTIuNiwxMzUuMi0yMC45YzEyLjctMi42LDIzLjYtNC45LDMwLjctNi4xYzcuOCwxLjQsMTkuNSwzLjksMzMuMSw2LjhDMzMwLDM2LjYsMzg1LjUsNDguNiw0MjAsNDguNiAgIGM4LjEsMCwxNC43LTAuNywxOS43LTJDNDM5LjMsMTIyLjksNDM0LjcsMjExLjUsNDA2LjMsMjk1Ljl6Ij48L3BhdGg+PHBhdGggZD0iTTI1Ni41LDIxNy44YzQ1LDAsODEuNi0zNi42LDgxLjYtODEuNmMwLTQ1LTM2LjYtODEuNi04MS42LTgxLjZjLTQ1LDAtODEuNiwzNi42LTgxLjYsODEuNiAgIEMxNzQuOSwxODEuMiwyMTEuNSwyMTcuOCwyNTYuNSwyMTcuOHogTTI1Ni41LDYyLjZjNDAuNiwwLDczLjYsMzMsNzMuNiw3My42YzAsNDAuNi0zMyw3My42LTczLjYsNzMuNmMtNDAuNiwwLTczLjYtMzMtNzMuNi03My42ICAgQzE4Mi45LDk1LjYsMjE1LjksNjIuNiwyNTYuNSw2Mi42eiI+PC9wYXRoPjxwYXRoIGQ9Ik0zMDkuMiwyMjguOUgyMDMuOGMtMjYuNSwwLTQ4LDIxLjUtNDgsNDh2NzljMCwyLjIsMS44LDQsNCw0aDE5My40YzIuMiwwLDQtMS44LDQtNHYtNzkgICBDMzU3LjIsMjUwLjQsMzM1LjYsMjI4LjksMzA5LjIsMjI4Ljl6IE0zNDkuMiwzNTEuOUgxNjMuOHYtNzVjMC0yMiwxNy45LTQwLDQwLTQwaDEwNS40YzIyLjEsMCw0MCwxNy45LDQwLDQwTDM0OS4yLDM1MS45ICAgTDM0OS4yLDM1MS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=",
|
||||
component: SecurityPage,
|
||||
},
|
||||
];
|
||||
|
||||
function getPage() {
|
||||
let pageid = window.location.hash.slice(1);
|
||||
return pages.find((e) => e.id === pageid) || pages[0];
|
||||
}
|
||||
|
||||
let page = getPage();
|
||||
window.addEventListener("hashchange", () => {
|
||||
page = getPage();
|
||||
});
|
||||
// $: title = pages.find(e => e.id === page).title;
|
||||
|
||||
const mq = window.matchMedia("(min-width: 45rem)");
|
||||
let sidebar_button = !mq.matches;
|
||||
mq.addEventListener("change", (ev) => {
|
||||
sidebar_button = !ev.matches;
|
||||
});
|
||||
|
||||
let sidebar_active = false;
|
||||
|
||||
function setPage(pageid) {
|
||||
let pg = pages.find((e) => e.id === pageid);
|
||||
if (!pg) {
|
||||
throw new Error("Invalid Page " + pageid);
|
||||
} else {
|
||||
let url = new URL(window.location.href);
|
||||
url.hash = pg.id;
|
||||
window.history.pushState({}, pg.title, url);
|
||||
page = getPage();
|
||||
}
|
||||
|
||||
sidebar_active = false;
|
||||
}
|
||||
|
||||
let loading = true;
|
||||
|
||||
import NavigationBar from "./NavigationBar.svelte";
|
||||
</script>
|
||||
|
||||
<div class:loading class="root">
|
||||
<div class="app_container">
|
||||
<div class="header">
|
||||
{#if sidebar_button}
|
||||
<button on:click={() => (sidebar_active = !sidebar_active)}>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
style="enable-background:new 0 0 32 32;"
|
||||
version="1.1"
|
||||
viewBox="0 0 32 32"
|
||||
width="32px"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<path
|
||||
d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z
|
||||
M28,14H4c-1.104,0-2,0.896-2,2
|
||||
s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z
|
||||
M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2
|
||||
S29.104,22,28,22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<h1>{page.title}</h1>
|
||||
</div>
|
||||
<div class="sidebar" class:sidebar-visible={sidebar_active}>
|
||||
<NavigationBar open={setPage} {pages} active={page} />
|
||||
</div>
|
||||
<div class="content">
|
||||
<svelte:component this={page.component} bind:loading />
|
||||
</div>
|
||||
<div class="footer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loader_container">
|
||||
<div class="loader_box">
|
||||
<div class="loader" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--sidebar-width: 250px;
|
||||
}
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app_container {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-template-columns: auto 100%;
|
||||
grid-template-rows: 60px auto 60px;
|
||||
grid-template-areas:
|
||||
"sidebar header"
|
||||
"sidebar mc"
|
||||
"sidebar footer";
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
background-color: var(--primary);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header > h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
color: white;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.header > button {
|
||||
height: 36px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header > button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.151);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
grid-area: sidebar;
|
||||
transition: width 0.2s;
|
||||
background-color: lightgrey;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-visible {
|
||||
width: var(--sidebar-width);
|
||||
transition: width 0.2s;
|
||||
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-area: mc;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
}
|
||||
|
||||
@media (min-width: 45rem) {
|
||||
.app_container {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
transition: all 0.2s;
|
||||
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loader_container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
@ -1,54 +0,0 @@
|
||||
<script>
|
||||
export let open;
|
||||
export let active;
|
||||
export let pages = [];
|
||||
</script>
|
||||
|
||||
{#each pages as page}
|
||||
<div
|
||||
class={"item_container" + (page === active ? " active" : "")}
|
||||
on:click={() => open(page.id)}
|
||||
>
|
||||
<div class="icon">
|
||||
<img alt={page.title} src={page.icon} />
|
||||
</div>
|
||||
<h3 class="title">{page.title}</h3>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--rel-size: 0.75rem;
|
||||
}
|
||||
|
||||
.item_container {
|
||||
height: calc(var(--rel-size) * 5);
|
||||
padding: var(--rel-size);
|
||||
display: flex;
|
||||
/* align-content: center; */
|
||||
align-items: center;
|
||||
/* justify-content: center; */
|
||||
}
|
||||
|
||||
.active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* float: left; */
|
||||
width: calc(var(--rel-size) * 3);
|
||||
height: calc(var(--rel-size) * 3);
|
||||
}
|
||||
|
||||
.icon > img {
|
||||
width: calc(var(--rel-size) * 3);
|
||||
height: calc(var(--rel-size) * 3);
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
/* margin: auto; */
|
||||
margin-left: var(--rel-size);
|
||||
/* height: 100%; */
|
||||
}
|
||||
</style>
|
@ -1,192 +0,0 @@
|
||||
<script>
|
||||
import Box from "./Box.svelte";
|
||||
import BoxItem from "./BoxItem.svelte";
|
||||
import NextIcon from "./NextIcon.svelte";
|
||||
|
||||
import request from "../../../helper/request.ts";
|
||||
|
||||
export let loading = false;
|
||||
let account_error = undefined;
|
||||
let contact_error = undefined;
|
||||
|
||||
const genderMap = new Map();
|
||||
genderMap.set(0, "None");
|
||||
genderMap.set(1, "Male");
|
||||
genderMap.set(2, "Female");
|
||||
genderMap.set(3, "Other");
|
||||
|
||||
let name = "";
|
||||
let gender = 0;
|
||||
$: genderHuman = genderMap.get(gender) || "ERROR";
|
||||
let birthday = undefined;
|
||||
|
||||
async function saveName() {
|
||||
//TODO: implement
|
||||
await load();
|
||||
}
|
||||
|
||||
async function saveGender() {
|
||||
//TODO: implement
|
||||
await load();
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
let { user } = await request(
|
||||
"/api/user/account",
|
||||
{},
|
||||
"GET",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
name = user.name;
|
||||
// username = user.username;
|
||||
gender = user.gender;
|
||||
birthday = user.birthday
|
||||
? new Date(user.birthday).toLocaleDateString()
|
||||
: undefined;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
account_error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
let email = [];
|
||||
let phone = [];
|
||||
|
||||
async function loadContact() {
|
||||
try {
|
||||
let { contact } = await request(
|
||||
"/api/user/contact",
|
||||
{},
|
||||
"GET",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
email = contact.mails.map((e) => e.mail);
|
||||
phone = contact.phones.map((e) => e.phone);
|
||||
contact_error = undefined;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
contact_error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
await Promise.all([loadProfile(), loadContact()]);
|
||||
loading = false;
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
background-color: var(--primary);
|
||||
margin: auto 0;
|
||||
margin-left: 1rem;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.floating {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-container > *:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: unset;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: unset;
|
||||
font-size: unset;
|
||||
border-bottom: 1px solid #757575;
|
||||
/* Firefox */
|
||||
-moz-appearance: none;
|
||||
/* Safari and Chrome */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select > option {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-wrapper::after {
|
||||
content: ">";
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1rem;
|
||||
transform: rotate(90deg) scaleY(2);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Box>
|
||||
<h1>Profile</h1>
|
||||
{#if account_error}
|
||||
<p class="error">{account_error}</p>
|
||||
{/if}
|
||||
<BoxItem name="Name" value={name}>
|
||||
<div class="input-container">
|
||||
<div class="floating group">
|
||||
<input
|
||||
id="name-inp"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
bind:value={name} />
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="name-inp">Name</label>
|
||||
</div>
|
||||
<button class="btn" on:click={saveName}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Gender" value={genderHuman}>
|
||||
<div class="input-container">
|
||||
<div class="select-wrapper">
|
||||
<select bind:value={gender}>
|
||||
<option value={1}>Male</option>
|
||||
<option value={2}>Female</option>
|
||||
<option value={3}>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" on:click={saveGender}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Birthday" value={birthday} />
|
||||
<BoxItem name="Password" value="******" />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<h1>Contact</h1>
|
||||
{#if contact_error}
|
||||
<p class="error">{contact_error}</p>
|
||||
{/if}
|
||||
<BoxItem name="E-Mail" value={email} noOpen={true} />
|
||||
<BoxItem name="Phone" value={phone} noOpen={true} />
|
||||
</Box>
|
@ -1,36 +0,0 @@
|
||||
<style>
|
||||
.box {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.30), 0 5px 4px rgba(0, 0, 0, 0.22);
|
||||
padding: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.box> :global(h1) {
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #444444;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.box> :global(div) {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.box> :global(div):first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@media (min-width: 45rem) {
|
||||
.box {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="box">
|
||||
<slot></slot>
|
||||
</div>
|
@ -1,94 +0,0 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
import NextIcon from "./NextIcon.svelte";
|
||||
export let name = "";
|
||||
export let value = "";
|
||||
export let noOpen = false;
|
||||
export let open = false;
|
||||
export let highlight = false;
|
||||
|
||||
function toggleOpen(ev) {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.root:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.values {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: calc(100% - var(--default-font-size) - 16px);
|
||||
}
|
||||
|
||||
.values > div:first-child {
|
||||
transform-origin: left;
|
||||
transform: scale(0.95);
|
||||
margin-right: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.values > div:nth-child(2) {
|
||||
color: black;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
margin: auto 8px auto 8px;
|
||||
height: var(--default-font-size);
|
||||
min-width: var(--default-font-size);
|
||||
}
|
||||
|
||||
.body {
|
||||
box-sizing: border-box;
|
||||
padding: 0.1px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 45rem) {
|
||||
.values {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.values > div:first-child {
|
||||
transform: unset;
|
||||
flex-basis: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-element {
|
||||
background-color: #7bff003b;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="root" class:highlight-element={highlight}>
|
||||
<div class="container" on:click={() => (open = !open)}>
|
||||
<div class="values">
|
||||
<div>{name}</div>
|
||||
<div>
|
||||
{#if Array.isArray(value)}
|
||||
{#each value as v, i}
|
||||
{v}
|
||||
{#if i < value.length - 1}
|
||||
<br />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}{value}{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if !noOpen}
|
||||
<NextIcon rotation={open ? -90 : 90} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if open && !noOpen}
|
||||
<div class="body" transition:slide>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
@ -1,13 +0,0 @@
|
||||
<script>
|
||||
export let rotation;
|
||||
</script>
|
||||
|
||||
<svg style={`enable-background:new 0 0 35.414 35.414; transform: rotate(${rotation}deg); transition: all .4s;`}
|
||||
version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 35.414 35.414" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="27.051,17 9.905,0 8.417,1.414 24.674,17.707 8.363,34 9.914,35.414 27.051,18.414 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
@ -1,188 +0,0 @@
|
||||
<script context="module">
|
||||
const TFATypes = new Map();
|
||||
TFATypes.set(0, "Authenticator");
|
||||
TFATypes.set(1, "Backup Codes");
|
||||
TFATypes.set(2, "YubiKey");
|
||||
TFATypes.set(3, "Push Notification");
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Box from "./Box.svelte";
|
||||
import BoxItem from "./BoxItem.svelte";
|
||||
import NextIcon from "./NextIcon.svelte";
|
||||
import request from "../../../helper/request.ts";
|
||||
|
||||
export let loading = false;
|
||||
|
||||
let twofactor = [];
|
||||
|
||||
async function deleteTFA(id) {
|
||||
let res = await request(
|
||||
"/api/user/twofactor/" + id,
|
||||
undefined,
|
||||
"DELETE",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
loadTwoFactor();
|
||||
}
|
||||
|
||||
async function loadTwoFactor() {
|
||||
let res = await request(
|
||||
"/api/user/twofactor",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
twofactor = res.methods;
|
||||
}
|
||||
|
||||
let token = [];
|
||||
|
||||
async function revoke(id) {
|
||||
let res = await request(
|
||||
"/api/user/token/" + id,
|
||||
undefined,
|
||||
"DELETE",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
loadToken();
|
||||
}
|
||||
|
||||
async function loadToken() {
|
||||
loading = true;
|
||||
let res = await request(
|
||||
"/api/user/token",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
token = res.token;
|
||||
loading = false;
|
||||
}
|
||||
|
||||
loadToken();
|
||||
loadTwoFactor();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
background-color: var(--primary);
|
||||
margin: auto 0;
|
||||
margin-left: 1rem;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.floating {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-container > *:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: unset;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: unset;
|
||||
font-size: unset;
|
||||
border-bottom: 1px solid #757575;
|
||||
/* Firefox */
|
||||
-moz-appearance: none;
|
||||
/* Safari and Chrome */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select > option {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-wrapper::after {
|
||||
content: ">";
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1rem;
|
||||
transform: rotate(90deg) scaleY(2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Box>
|
||||
<h1>Two Factor</h1>
|
||||
<BoxItem name="Add new" open={false} />
|
||||
{#each twofactor as t}
|
||||
<BoxItem name={TFATypes.get(t.type)} value={t.name} highlight={t.isthis}>
|
||||
<button
|
||||
class="btn"
|
||||
style="background: var(--error)"
|
||||
on:click={() => deleteTFA(t.id)}>
|
||||
Delete
|
||||
</button>
|
||||
</BoxItem>
|
||||
{/each}
|
||||
<!-- <BoxItem name="Name" value={name} open={false}>
|
||||
<div class="input-container">
|
||||
<div class="floating group">
|
||||
<input type="text" autocomplete="username" bind:value={name}>
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label>Name</label>
|
||||
</div>
|
||||
<button class="btn" on:click={saveName}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Gender" value={gender} open={true}>
|
||||
<div class="input-container">
|
||||
<div class="select-wrapper">
|
||||
<select>
|
||||
<option value="1">Male</option>
|
||||
<option value="2">Female</option>
|
||||
<option value="3">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" on:click={saveName}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Birthday" value={birthday} />
|
||||
<BoxItem name="Password" value="******" /> -->
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<h1>Anmeldungen</h1>
|
||||
|
||||
{#each token as t}
|
||||
<BoxItem name={t.browser} value={t.ip} highlight={t.isthis}>
|
||||
<button
|
||||
class="btn"
|
||||
style="background: var(--error)"
|
||||
on:click={() => revoke(t.id)}>
|
||||
Revoke
|
||||
</button>
|
||||
</BoxItem>
|
||||
{:else}<span>No Tokens</span>{/each}
|
||||
|
||||
<!-- <BoxItem name="E-Mail" value={email} />
|
||||
<BoxItem name="Phone" value={phone} /> -->
|
||||
</Box>
|
@ -1,6 +0,0 @@
|
||||
import "../../components/theme";
|
||||
import App from "./App.svelte";
|
||||
|
||||
new App({
|
||||
target: document.body,
|
||||
});
|
@ -23,6 +23,6 @@
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"sass": "^1.61.0",
|
||||
"typescript": "^5.0.3"
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
16
InternalAPI/account.jrpc
Normal file
16
InternalAPI/account.jrpc
Normal file
@ -0,0 +1,16 @@
|
||||
type UserRegisterInfo {
|
||||
username: string;
|
||||
name: string;
|
||||
gender: string;
|
||||
mail: string;
|
||||
password: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
service AccountService {
|
||||
Register(regcode: string, info: UserRegisterInfo): void;
|
||||
GetProfile(): Profile;
|
||||
UpdateProfile(info: Profile): void;
|
||||
GetContactInfos(): ContactInfo;
|
||||
}
|
||||
|
@ -1,77 +1,5 @@
|
||||
type UserRegisterInfo {
|
||||
username: string;
|
||||
name: string;
|
||||
gender: string;
|
||||
mail: string;
|
||||
password: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
type Token {
|
||||
id: string;
|
||||
special: boolean;
|
||||
ip: string;
|
||||
browser: string;
|
||||
isthis: boolean;
|
||||
}
|
||||
|
||||
enum Gender {
|
||||
None = 0,
|
||||
Male = 1,
|
||||
Female = 2,
|
||||
Other = 3
|
||||
}
|
||||
|
||||
type Account {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
birthday: int;
|
||||
gender: Gender;
|
||||
}
|
||||
|
||||
type Mail {
|
||||
mail: string;
|
||||
verified: boolean;
|
||||
primary: boolean;
|
||||
}
|
||||
|
||||
type Phone {
|
||||
phone: string;
|
||||
verified: boolean;
|
||||
primary: boolean;
|
||||
}
|
||||
|
||||
type ContactInfo {
|
||||
mail: Mail[];
|
||||
phone: Phone[];
|
||||
}
|
||||
|
||||
enum TFAType {
|
||||
TOTP = 0,
|
||||
BACKUP_CODE = 1,
|
||||
WEBAUTHN = 2,
|
||||
APP_ALLOW = 3
|
||||
}
|
||||
|
||||
|
||||
type TwoFactor {
|
||||
id: string;
|
||||
name?: string;
|
||||
expires?: int;
|
||||
tfatype: TFAType;
|
||||
}
|
||||
|
||||
service AccountService {
|
||||
Register(regcode: string, info: UserRegisterInfo): void;
|
||||
GetProfile(): Account;
|
||||
UpdateProfile(info: Account): void;
|
||||
GetContactInfos(): ContactInfo;
|
||||
}
|
||||
|
||||
service SecurityService {
|
||||
GetTokens(): Token[];
|
||||
RevokeToken(id: string): void;
|
||||
|
||||
GetTwofactorOptions(): TwoFactor[];
|
||||
}
|
||||
import "./types";
|
||||
import "./twofactor";
|
||||
import "./login";
|
||||
import "./account";
|
||||
import "./security";
|
||||
|
21
InternalAPI/login.jrpc
Normal file
21
InternalAPI/login.jrpc
Normal file
@ -0,0 +1,21 @@
|
||||
import "./twofactor";
|
||||
|
||||
type LoginState {
|
||||
success: boolean;
|
||||
username?: string;
|
||||
password?: boolean;
|
||||
passwordSalt?: string;
|
||||
requireTwoFactor?: TFAOption[];
|
||||
}
|
||||
|
||||
|
||||
service LoginService {
|
||||
GetState(): LoginState;
|
||||
Start(username: string): LoginState;
|
||||
UsePassword(password_hash: string, date: int): LoginState;
|
||||
|
||||
UseTOTP(id: string, code: string): LoginState;
|
||||
UseBackupCode(id: string, code:string): LoginState;
|
||||
GetWebAuthnChallenge(id: string): string;
|
||||
UseWebAuthn(id: string, response: string): LoginState;
|
||||
}
|
14
InternalAPI/security.jrpc
Normal file
14
InternalAPI/security.jrpc
Normal file
@ -0,0 +1,14 @@
|
||||
type Session {
|
||||
id: string;
|
||||
special: boolean;
|
||||
ip: string;
|
||||
browser: string;
|
||||
isthis: boolean;
|
||||
}
|
||||
|
||||
service SecurityService {
|
||||
GetSessions(): Session[];
|
||||
RevokeSession(id: string): void;
|
||||
|
||||
ChangePassword(old: string, new_pw: string): void;
|
||||
}
|
38
InternalAPI/twofactor.jrpc
Normal file
38
InternalAPI/twofactor.jrpc
Normal file
@ -0,0 +1,38 @@
|
||||
enum TFAType {
|
||||
TOTP = 0,
|
||||
BACKUP_CODE = 1,
|
||||
WEBAUTHN = 2,
|
||||
APP_ALLOW = 3
|
||||
}
|
||||
|
||||
type TFAOption {
|
||||
id: string;
|
||||
name?: string;
|
||||
expires?: int;
|
||||
tfatype: TFAType;
|
||||
}
|
||||
|
||||
type TFANewTOTP {
|
||||
id: string;
|
||||
secret: string;
|
||||
qr: string;
|
||||
}
|
||||
|
||||
type TFAWebAuthRegister {
|
||||
id: string;
|
||||
challenge: string;
|
||||
}
|
||||
|
||||
service TFAService {
|
||||
GetOptions(): TFAOption[];
|
||||
Delete(id: string): void;
|
||||
|
||||
AddTOTP(name: string): TFANewTOTP;
|
||||
VerifyTOTP(id: string, code: string): void;
|
||||
|
||||
AddWebauthn(name: string): TFAWebAuthRegister;
|
||||
VerifyWebAuthn(id: string, registration_response: string): void;
|
||||
|
||||
AddBackupCodes(name:string): string[];
|
||||
RemoveBackupCodes(id: string): void;
|
||||
}
|
31
InternalAPI/types.jrpc
Normal file
31
InternalAPI/types.jrpc
Normal file
@ -0,0 +1,31 @@
|
||||
enum Gender {
|
||||
None = 0,
|
||||
Male = 1,
|
||||
Female = 2,
|
||||
Other = 3
|
||||
}
|
||||
|
||||
type Profile {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
birthday: int;
|
||||
gender: Gender;
|
||||
}
|
||||
|
||||
type Mail {
|
||||
mail: string;
|
||||
verified: boolean;
|
||||
primary: boolean;
|
||||
}
|
||||
|
||||
type Phone {
|
||||
phone: string;
|
||||
verified: boolean;
|
||||
primary: boolean;
|
||||
}
|
||||
|
||||
type ContactInfo {
|
||||
mail: Mail[];
|
||||
phone: Phone[];
|
||||
}
|
@ -14,6 +14,6 @@
|
||||
"author": "Fabian Stamm <Fabian.Stamm@polizei.hessen.de>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.2"
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
"author": "Fabian Stamm <dev@fabianstamm.de>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "yarn run build-views-1 && yarn run build-views-2 && yarn run build-backend",
|
||||
"build": "yarn build-api && yarn run build-views-1 && yarn run build-views-2 && yarn run build-backend",
|
||||
"build-api": "jrpc compile ./InternalAPI/api.jrpc -o=ts-node:_API/src && yarn workspace @hibas123/openauth-internalapi run build",
|
||||
"build-backend": "yarn workspace @hibas123/openauth-backend run build",
|
||||
"build-views-1": "yarn workspace @hibas123/openauth-views-v1 run build",
|
||||
|
Loading…
Reference in New Issue
Block a user