Add JRPC API, reworked Login and User pages

This commit is contained in:
Fabian Stamm 2023-04-14 15:13:53 +02:00
parent 922ed1e813
commit e1164eb05b
99 changed files with 4570 additions and 5471 deletions

View File

@ -3,14 +3,7 @@ type: docker
name: default name: default
steps: steps:
- name: Build with node - name: Build docker
image: node:12
commands:
- npm config set registry https://npm.hibas123.de
- npm install
- npm run install
- npm run build
- name: Publish to docker
image: plugins/docker image: plugins/docker
settings: settings:
username: username:

16
.vscode/launch.json vendored
View File

@ -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
View File

@ -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"
}
]
}

View File

@ -1,15 +1,16 @@
[database] [database]
host=localhost host=localhost
database=openauth database=openauth
[core] [core]
name = OpenAuthService name = OpenAuthService
secret = dev
[web]
port = 3000 [web]
port = 3000
[mail]
server = mail.example.com [mail]
username = test server = mail.example.com
password = test username = test
port = 595 password = test
port = 595

View File

@ -39,5 +39,10 @@
"No login token": "No login token", "No login token": "No login token",
"You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)", "You are not logged in or your login is expired (Login token invalid)": "You are not logged in or your login is expired (Login token invalid)",
"You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)", "You are not logged in or your login is expired (No special token)": "You are not logged in or your login is expired (No special token)",
"Special token invalid": "Special token invalid" "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"
} }

View File

@ -1,72 +1,77 @@
{ {
"name": "@hibas123/openauth-backend", "name": "@hibas123/openauth-backend",
"main": "lib/index.js", "main": "lib/index.js",
"author": "Fabian Stamm <dev@fabianstamm.de>", "author": "Fabian Stamm <dev@fabianstamm.de>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "run-s build-ts build-doc", "build": "run-s build-ts build-doc",
"build-doc": "apidoc -i src/ -p apidoc/", "build-doc": "apidoc -i src/ -p apidoc/",
"build-ts": "tsc", "build-ts": "tsc",
"start": "node lib/index.js", "start": "node lib/index.js",
"dev": "nodemon -e ts --exec ts-node src/index.ts", "dev": "nodemon -e ts --exec ts-node src/index.ts",
"format": "prettier --write \"src/**\"" "format": "prettier --write \"src/**\""
}, },
"pipelines": { "pipelines": {
"install": [ "install": [
"cd views && npm install", "cd views && npm install",
"git submodule init", "git submodule init",
"git submodule update", "git submodule update",
"cd views_repo && npm install" "cd views_repo && npm install"
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/i18n": "^0.13.6", "@types/express-session": "^1.17.7",
"@types/ini": "^1.3.31", "@types/i18n": "^0.13.6",
"@types/jsonwebtoken": "^9.0.1", "@types/ini": "^1.3.31",
"@types/mongodb": "^3.6.20", "@types/jsonwebtoken": "^9.0.1",
"@types/node": "^18.15.11", "@types/mongodb": "^4.0.7",
"@types/node-rsa": "^1.1.1", "@types/node": "^18.15.11",
"@types/qrcode": "^1.5.0", "@types/node-rsa": "^1.1.1",
"@types/speakeasy": "^2.0.7", "@types/qrcode": "^1.5.0",
"@types/uuid": "^9.0.1", "@types/speakeasy": "^2.0.7",
"apidoc": "^0.54.0", "@types/uuid": "^9.0.1",
"concurrently": "^8.0.1", "apidoc": "^0.54.0",
"nodemon": "^2.0.22", "concurrently": "^8.0.1",
"prettier": "^2.8.7", "nodemon": "^2.0.22",
"ts-node": "^10.9.1", "prettier": "^2.8.7",
"typescript": "^5.0.3" "ts-node": "^10.9.1",
}, "typescript": "^5.0.4"
"dependencies": { },
"@hibas123/config": "^1.1.2", "dependencies": {
"@hibas123/nodelogging": "^3.1.3", "@hibas123/config": "^1.1.2",
"@hibas123/nodeloggingserver_client": "^1.1.2", "@hibas123/nodelogging": "^3.1.3",
"@hibas123/openauth-internalapi": "workspace:^", "@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/openauth-views-v1": "workspace:^", "@hibas123/openauth-internalapi": "workspace:^",
"@hibas123/safe_mongo": "^1.7.1", "@hibas123/openauth-views-v1": "workspace:^",
"body-parser": "^1.20.2", "@hibas123/safe_mongo": "^2.0.1",
"compression": "^1.7.4", "@simplewebauthn/server": "^7.2.0",
"cookie-parser": "^1.4.6", "body-parser": "^1.20.2",
"cors": "^2.8.5", "compression": "^1.7.4",
"dotenv": "^16.0.3", "connect-mongo": "^5.0.0",
"express": "^4.18.2", "cookie-parser": "^1.4.6",
"handlebars": "^4.7.7", "cors": "^2.8.5",
"i18n": "^0.15.1", "dotenv": "^16.0.3",
"ini": "^4.0.0", "express": "^4.18.2",
"jsonwebtoken": "^9.0.0", "express-session": "^1.17.3",
"moment": "^2.29.4", "handlebars": "^4.7.7",
"mongodb": "^3.7.3", "i18n": "^0.15.1",
"node-rsa": "^1.1.1", "ini": "^4.0.0",
"npm-run-all": "^4.1.5", "joi": "^17.9.1",
"qrcode": "^1.5.1", "jsonwebtoken": "^9.0.0",
"reflect-metadata": "^0.1.13", "moment": "^2.29.4",
"speakeasy": "^2.0.0", "mongodb": "^5.2.0",
"u2f": "^0.1.3", "node-rsa": "^1.1.1",
"uuid": "^9.0.0" "npm-run-all": "^4.1.5",
}, "qrcode": "^1.5.1",
"packageManager": "yarn@3.5.0" "reflect-metadata": "^0.1.13",
} "speakeasy": "^2.0.0",
"u2f": "^0.1.3",
"uuid": "^9.0.0"
},
"packageManager": "yarn@3.5.0"
}

View File

@ -1,111 +1,111 @@
import { Request, Router } from "express"; import { Request, Router } from "express";
import { GetUserMiddleware } from "../middlewares/user"; import { GetUserMiddleware } from "../middlewares/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware"; import promiseMiddleware from "../../helper/promiseMiddleware";
import Permission from "../../models/permissions"; import Permission from "../../models/permissions";
import verify, { Types } from "../middlewares/verify"; import verify, { Types } from "../middlewares/verify";
import Client from "../../models/client"; import Client from "../../models/client";
import { ObjectID } from "bson"; import { ObjectId } from "bson";
const PermissionRoute: Router = Router(); const PermissionRoute: Router = Router();
PermissionRoute.route("/") PermissionRoute.route("/")
/** /**
* @api {get} /admin/permission * @api {get} /admin/permission
* @apiName AdminGetPermissions * @apiName AdminGetPermissions
* *
* @apiParam client Optionally filter by client _id * @apiParam client Optionally filter by client _id
* *
* @apiGroup admin_permission * @apiGroup admin_permission
* @apiPermission admin * @apiPermission admin
* *
* @apiSuccess {Object[]} permissions * @apiSuccess {Object[]} permissions
* @apiSuccess {String} permissions._id The ID * @apiSuccess {String} permissions._id The ID
* @apiSuccess {String} permissions.name Permission name * @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.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.client The ID of the owning client
*/ */
.get( .get(
promiseMiddleware(async (req, res) => { promiseMiddleware(async (req, res) => {
let query = {}; let query = {};
if (req.query.client) { if (req.query.client) {
query = { client: new ObjectID(req.query.client as string) }; query = { client: new ObjectId(req.query.client as string) };
} }
let permissions = await Permission.find(query); let permissions = await Permission.find(query);
res.json(permissions); res.json(permissions);
}) })
) )
/** /**
* @api {post} /admin/permission * @api {post} /admin/permission
* @apiName AdminAddPermission * @apiName AdminAddPermission
* *
* @apiParam client The ID of the owning client * @apiParam client The ID of the owning client
* @apiParam name Permission name * @apiParam name Permission name
* @apiParam description A description, that makes it clear to the user, what this Permission allows to do * @apiParam description A description, that makes it clear to the user, what this Permission allows to do
* *
* @apiGroup admin_permission * @apiGroup admin_permission
* @apiPermission admin * @apiPermission admin
* *
* @apiSuccess {Object[]} permissions * @apiSuccess {Object[]} permissions
* @apiSuccess {String} permissions._id The ID * @apiSuccess {String} permissions._id The ID
* @apiSuccess {String} permissions.name Permission name * @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.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.client The ID of the owning client
* @apiSuccess {String} permissions.grant_type The type of the permission. "user" | "client" granted * @apiSuccess {String} permissions.grant_type The type of the permission. "user" | "client" granted
*/ */
.post( .post(
verify( verify(
{ {
client: { client: {
type: Types.STRING, type: Types.STRING,
}, },
name: { name: {
type: Types.STRING, type: Types.STRING,
}, },
description: { description: {
type: Types.STRING, type: Types.STRING,
}, },
type: { type: {
type: Types.ENUM, type: Types.ENUM,
values: ["user", "client"], values: ["user", "client"],
}, },
}, },
true true
), ),
promiseMiddleware(async (req, res) => { promiseMiddleware(async (req, res) => {
let client = await Client.findById(req.body.client); let client = await Client.findById(req.body.client);
if (!client) { if (!client) {
throw new RequestError( throw new RequestError(
"Client not found", "Client not found",
HttpStatusCode.BAD_REQUEST HttpStatusCode.BAD_REQUEST
); );
} }
let permission = Permission.new({ let permission = Permission.new({
description: req.body.description, description: req.body.description,
name: req.body.name, name: req.body.name,
client: client._id, client: client._id,
grant_type: req.body.type, grant_type: req.body.type,
}); });
await Permission.save(permission); await Permission.save(permission);
res.json(permission); res.json(permission);
}) })
) )
/** /**
* @api {delete} /admin/permission * @api {delete} /admin/permission
* @apiName AdminDeletePermission * @apiName AdminDeletePermission
* *
* @apiParam id The permission ID * @apiParam id The permission ID
* *
* @apiGroup admin_permission * @apiGroup admin_permission
* @apiPermission admin * @apiPermission admin
* *
* @apiSuccess {Boolean} success * @apiSuccess {Boolean} success
*/ */
.delete( .delete(
promiseMiddleware(async (req, res) => { promiseMiddleware(async (req, res) => {
let { id } = req.query as { [key: string]: string }; let { id } = req.query as { [key: string]: string };
await Permission.delete(id); await Permission.delete(id);
res.json({ success: true }); res.json({ success: true });
}) })
); );
export default PermissionRoute; export default PermissionRoute;

View File

@ -1,98 +1,98 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import { import {
ClientAuthMiddleware, ClientAuthMiddleware,
GetClientAuthMiddleware, GetClientAuthMiddleware,
} from "../middlewares/client"; } from "../middlewares/client";
import Permission from "../../models/permissions"; import Permission from "../../models/permissions";
import User from "../../models/user"; import User from "../../models/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../helper/request_error";
import Grant from "../../models/grants"; import Grant from "../../models/grants";
import { ObjectID } from "mongodb"; import { ObjectId } from "mongodb";
export const GetPermissions = Stacker( export const GetPermissions = Stacker(
GetClientAuthMiddleware(true), GetClientAuthMiddleware(true),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { user, permission } = req.query as { [key: string]: string }; const { user, permission } = req.query as { [key: string]: string };
let permissions: { id: string; name: string; description: string }[]; let permissions: { id: string; name: string; description: string }[];
let users: string[]; let users: string[];
if (user) { if (user) {
const grant = await Grant.findOne({ const grant = await Grant.findOne({
client: req.client._id, client: req.client._id,
user: new ObjectID(user), user: new ObjectId(user),
}); });
permissions = await Promise.all( permissions = await Promise.all(
grant.permissions.map((perm) => Permission.findById(perm)) grant.permissions.map((perm) => Permission.findById(perm))
).then((res) => ).then((res) =>
res res
.filter((e) => e.grant_type === "client") .filter((e) => e.grant_type === "client")
.map((e) => { .map((e) => {
return { return {
id: e._id.toHexString(), id: e._id.toHexString(),
name: e.name, name: e.name,
description: e.description, description: e.description,
}; };
}) })
); );
} }
if (permission) { if (permission) {
const grants = await Grant.find({ const grants = await Grant.find({
client: req.client._id, client: req.client._id,
permissions: new ObjectID(permission), permissions: new ObjectId(permission),
}); });
users = grants.map((grant) => grant.user.toHexString()); users = grants.map((grant) => grant.user.toHexString());
} }
res.json({ permissions, users }); res.json({ permissions, users });
} }
); );
export const PostPermissions = Stacker( export const PostPermissions = Stacker(
GetClientAuthMiddleware(true), GetClientAuthMiddleware(true),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { permission, uid } = req.body; const { permission, uid } = req.body;
const user = await User.findOne({ uid }); const user = await User.findOne({ uid });
if (!user) { if (!user) {
throw new RequestError("User not found!", HttpStatusCode.BAD_REQUEST); throw new RequestError("User not found!", HttpStatusCode.BAD_REQUEST);
} }
const permissionDoc = await Permission.findById(permission); const permissionDoc = await Permission.findById(permission);
if (!permissionDoc || !permissionDoc.client.equals(req.client._id)) { if (!permissionDoc || !permissionDoc.client.equals(req.client._id)) {
throw new RequestError( throw new RequestError(
"Permission not found!", "Permission not found!",
HttpStatusCode.BAD_REQUEST HttpStatusCode.BAD_REQUEST
); );
} }
let grant = await Grant.findOne({ let grant = await Grant.findOne({
client: req.client._id, client: req.client._id,
user: req.user._id, user: req.user._id,
}); });
if (!grant) { if (!grant) {
grant = Grant.new({ grant = Grant.new({
client: req.client._id, client: req.client._id,
user: req.user._id, user: req.user._id,
permissions: [], permissions: [],
}); });
} }
//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 //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) if (grant.permissions.indexOf(permission) < 0)
grant.permissions.push(permission); grant.permissions.push(permission);
await Grant.save(grant); await Grant.save(grant);
res.json({ res.json({
success: true, success: true,
}); });
} }
); );

View File

@ -1,54 +1,50 @@
import * as express from "express"; import * as express from "express";
import AdminRoute from "./admin"; import AdminRoute from "./admin";
import UserRoute from "./user"; import UserRoute from "./user";
import InternalRoute from "./internal"; import InternalRoute from "./internal";
import Login from "./user/login"; import ClientRouter from "./client";
import ClientRouter from "./client"; import cors from "cors";
import * as cors from "cors"; import OAuthRoute from "./oauth";
import OAuthRoute from "./oauth"; import config from "../config";
import config from "../config"; import JRPCEndpoint from "./jrpc";
import JRPCEndpoint from "./jrpc";
const ApiRouter: express.IRouter = express.Router();
const ApiRouter: express.IRouter = express.Router(); ApiRouter.use("/admin", AdminRoute);
ApiRouter.use("/admin", AdminRoute); ApiRouter.use(cors());
ApiRouter.use(cors()); ApiRouter.use("/user", UserRoute);
ApiRouter.use("/user", UserRoute); ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/internal", InternalRoute); ApiRouter.use("/oauth", OAuthRoute);
ApiRouter.use("/oauth", OAuthRoute);
ApiRouter.use("/client", ClientRouter);
ApiRouter.use("/client", ClientRouter);
/**
/** * @api {post} /jrpc
* @api {post} /jrpc * @apiName InternalJRPCEndpoint
* @apiName InternalJRPCEndpoint *
* * @apiGroup user
* @apiGroup user * @apiPermission none
* @apiPermission none *
* * @apiErrorExample {Object} Error-Response:
* @apiErrorExample {Object} Error-Response: {
{ error: [
error: [ {
{ message: "Some Error",
message: "Some Error", field: "username"
field: "username" }
} ],
], status: 400
status: 400 }
} */
*/ ApiRouter.post("/jrpc", JRPCEndpoint);
ApiRouter.post("/jrpc", JRPCEndpoint);
// Legacy reasons (deprecated)
// Legacy reasons (deprecated) ApiRouter.use("/", ClientRouter);
ApiRouter.use("/", ClientRouter);
ApiRouter.get("/config.json", (req, res) => {
// Legacy reasons (deprecated) return res.json({
ApiRouter.post("/login", Login); name: config.core.name,
url: config.core.url,
ApiRouter.get("/config.json", (req, res) => { });
return res.json({ });
name: config.core.name,
url: config.core.url, export default ApiRouter;
});
});
export default ApiRouter;

View File

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

View File

@ -1,45 +1,38 @@
import { Request, Response } from "express"; import { Format } from "@hibas123/logging";
import Stacker from "../middlewares/stacker"; import Logging from "@hibas123/nodelogging";
import { GetUserMiddleware } from "../middlewares/user"; import { Server, } from "@hibas123/openauth-internalapi";
import { IUser } from "../../models/user"; import { RequestObject, ResponseObject } from "@hibas123/openauth-internalapi/lib/service_base";
import { Server } from "@hibas123/openauth-internalapi"; import { Request, Response } from "express";
import AccountService from "./account_service"; import Stacker from "../middlewares/stacker";
import SecurityService from "./security_service"; import AccountService from "./services/account";
import { ILoginToken } from "../../models/login_token"; import LoginService from "./services/login";
import SecurityService from "./services/security";
export interface SessionContext { import TFAService from "./services/twofactor";
user: IUser,
request: Request, export type SessionContext = Request;
isAdmin: boolean,
special: boolean, const provider = new Server.ServiceProvider<SessionContext>();
token: { provider.addService(new AccountService());
login: ILoginToken, provider.addService(new SecurityService());
special?: ILoginToken, provider.addService(new TFAService());
} provider.addService(new LoginService());
}
const JRPCEndpoint = Stacker(
const provider = new Server.ServiceProvider<SessionContext>(); async (req: Request, res: Response) => {
provider.addService(new AccountService()); let jrpcreq = req.body as RequestObject;
provider.addService(new SecurityService()); let startTime = process.hrtime.bigint();
const session = provider.getSession((data: ResponseObject) => {
const JRPCEndpoint = Stacker( let time = process.hrtime.bigint() - startTime;
GetUserMiddleware(true, true), let state = data.error ? Format.red(`err(${data.error.message})`) : Format.green("OK");
async (req: Request, res: Response) => {
const session = provider.getSession((data) => { Logging.getChild("JRPC").log(jrpcreq.method, state, "-", (Number(time / 10000n) / 100) + "ms");
res.json(data);
}, { res.json(data);
user: req.user, }, req);
request: req,
isAdmin: req.isAdmin,
special: req.special, session.onMessage(jrpcreq);
token: { }
login: req.token.login, );
special: req.token.special,
} export default JRPCEndpoint;
});
session.onMessage(req.body);
}
);
export default JRPCEndpoint;

View File

@ -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;
}
}

View File

@ -1,49 +1,52 @@
import { Account, ContactInfo, Gender, Server, UserRegisterInfo } from "@hibas123/openauth-internalapi"; import { Profile, ContactInfo, Gender, Server, UserRegisterInfo } from "@hibas123/openauth-internalapi";
import type { SessionContext } from "./index"; import type { SessionContext } from "../index";
import Mail from "../../models/mail"; import Mail from "../../../models/mail";
import User from "../../models/user"; import User from "../../../models/user";
import { RequireLogin } from "../../../helper/login";
export default class AccountService extends Server.AccountService<SessionContext> {
export default class AccountService extends Server.AccountService<SessionContext> {
Register(regcode: string, info: UserRegisterInfo, ctx: SessionContext): Promise<void> { Register(regcode: string, info: UserRegisterInfo, ctx: SessionContext): Promise<void> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
async GetProfile(ctx: SessionContext): Promise<Account> { @RequireLogin()
if (!ctx.user) throw new Error("Not logged in"); async GetProfile(ctx: SessionContext): Promise<Profile> {
if (!ctx.user) throw new Error("Not logged in");
return {
id: ctx.user.uid, return {
username: ctx.user.username, id: ctx.user.uid,
name: ctx.user.name, username: ctx.user.username,
birthday: ctx.user.birthday.valueOf(), name: ctx.user.name,
gender: ctx.user.gender as number as Gender, 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"); @RequireLogin()
async UpdateProfile(info: Profile, ctx: SessionContext): Promise<void> {
ctx.user.name = info.name; if (!ctx.user) throw new Error("Not logged in");
ctx.user.birthday = new Date(info.birthday);
ctx.user.gender = info.gender as number; ctx.user.name = info.name;
ctx.user.birthday = new Date(info.birthday);
await User.save(ctx.user); 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");
@RequireLogin()
let mails = await Promise.all( async GetContactInfos(ctx: SessionContext): Promise<ContactInfo> {
ctx.user.mails.map((mail) => Mail.findById(mail)) if (!ctx.user) throw new Error("Not logged in");
);
let mails = await Promise.all(
let contact = { ctx.user.mails.map((mail) => Mail.findById(mail))
mail: mails.filter((e) => !!e), );
phone: ctx.user.phones,
}; let contact = {
mail: mails.filter((e) => !!e),
return contact; phone: ctx.user.phones,
} };
}
return contact;
}
}

View 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);
}
}

View 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);
}
}

View 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");
}
}
}

View File

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

View File

@ -1,249 +1,249 @@
import Stacker from "../middlewares/stacker"; import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user"; import { GetUserMiddleware } from "../middlewares/user";
import { Request, Response } from "express"; import { Request, Response } from "express";
import Client from "../../models/client"; import Client from "../../models/client";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import Permission, { IPermission } from "../../models/permissions"; import Permission, { IPermission } from "../../models/permissions";
import ClientCode from "../../models/client_code"; import ClientCode from "../../models/client_code";
import moment = require("moment"); import moment = require("moment");
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
// import { ObjectID } from "bson"; // import { ObjectId } from "bson";
import Grant, { IGrant } from "../../models/grants"; import Grant, { IGrant } from "../../models/grants";
import GetAuthPage from "../../views/authorize"; import GetAuthPage from "../../views/authorize";
import { ObjectID } from "mongodb"; import { ObjectId } from "mongodb";
// const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => { // const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => {
// let { response_type, client_id, redirect_uri, scope, state, nored } = req.query; // let { response_type, client_id, redirect_uri, scope, state, nored } = req.query;
// const sendError = (type) => { // const sendError = (type) => {
// if (redirect_uri === "$local") // if (redirect_uri === "$local")
// redirect_uri = "/code"; // redirect_uri = "/code";
// res.redirect(redirect_uri += `?error=${type}&state=${state}`); // res.redirect(redirect_uri += `?error=${type}&state=${state}`);
// } // }
// /** // /**
// * error // * error
// REQUIRED. A single ASCII [USASCII] error code from the // REQUIRED. A single ASCII [USASCII] error code from the
// following: // following:
// invalid_request // invalid_request
// The request is missing a required parameter, includes an // The request is missing a required parameter, includes an
// invalid parameter value, includes a parameter more than // invalid parameter value, includes a parameter more than
// once, or is otherwise malformed. // once, or is otherwise malformed.
// unauthorized_client // unauthorized_client
// The client is not authorized to request an authorization // The client is not authorized to request an authorization
// code using this method. // code using this method.
// access_denied // access_denied
// The resource owner or authorization server denied the // The resource owner or authorization server denied the
// request. // request.
// */ // */
// try { // try {
// if (response_type !== "code") { // if (response_type !== "code") {
// return sendError("unsupported_response_type"); // return sendError("unsupported_response_type");
// } else { // } else {
// let client = await Client.findOne({ client_id: client_id }) // let client = await Client.findOne({ client_id: client_id })
// if (!client) { // if (!client) {
// return sendError("unauthorized_client") // return sendError("unauthorized_client")
// } // }
// if (redirect_uri && client.redirect_url !== redirect_uri) { // if (redirect_uri && client.redirect_url !== redirect_uri) {
// Logging.log(redirect_uri, client.redirect_url); // 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!"); // 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 permissions: IPermission[] = [];
// if (scope) { // if (scope) {
// let perms = (<string>scope).split(";").filter(e => e !== "read_user").map(p => new ObjectID(p)); // let perms = (<string>scope).split(";").filter(e => e !== "read_user").map(p => new ObjectId(p));
// permissions = await Permission.find({ _id: { $in: perms } }) // permissions = await Permission.find({ _id: { $in: perms } })
// if (permissions.length != perms.length) { // if (permissions.length != perms.length) {
// return sendError("invalid_scope"); // return sendError("invalid_scope");
// } // }
// } // }
// let code = ClientCode.new({ // let code = ClientCode.new({
// user: req.user._id, // user: req.user._id,
// client: client._id, // client: client._id,
// permissions: permissions.map(p => p._id), // permissions: permissions.map(p => p._id),
// validTill: moment().add(30, "minutes").toDate(), // validTill: moment().add(30, "minutes").toDate(),
// code: randomBytes(16).toString("hex") // code: randomBytes(16).toString("hex")
// }); // });
// await ClientCode.save(code); // await ClientCode.save(code);
// let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url; // let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url;
// let ruri = redir + `?code=${code.code}&state=${state}`; // let ruri = redir + `?code=${code.code}&state=${state}`;
// if (nored === "true") { // if (nored === "true") {
// res.json({ // res.json({
// redirect_uri: ruri // redirect_uri: ruri
// }) // })
// } else { // } else {
// res.redirect(ruri); // res.redirect(ruri);
// } // }
// } // }
// } catch (err) { // } catch (err) {
// Logging.error(err); // Logging.error(err);
// sendError("server_error") // sendError("server_error")
// } // }
// }) // })
const GetAuthRoute = (view = false) => const GetAuthRoute = (view = false) =>
Stacker(GetUserMiddleware(false), async (req: Request, res: Response) => { Stacker(GetUserMiddleware(false), async (req: Request, res: Response) => {
let { let {
response_type, response_type,
client_id, client_id,
redirect_uri, redirect_uri,
scope = "", scope = "",
state, state,
nored, nored,
} = req.query as { [key: string]: string }; } = req.query as { [key: string]: string };
const sendError = (type) => { const sendError = (type) => {
if (redirect_uri === "$local") redirect_uri = "/code"; if (redirect_uri === "$local") redirect_uri = "/code";
res.redirect( res.redirect(
(redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`) (redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)
); );
}; };
const scopes = scope.split(";").filter((e: string) => e !== ""); const scopes = scope.split(";").filter((e: string) => e !== "");
Logging.debug("Scopes:", scope); Logging.debug("Scopes:", scope);
try { try {
if (response_type !== "code") { if (response_type !== "code") {
return sendError("unsupported_response_type"); return sendError("unsupported_response_type");
} else { } else {
let client = await Client.findOne({ client_id: client_id }); let client = await Client.findOne({ client_id: client_id });
if (!client) { if (!client) {
return sendError("unauthorized_client"); return sendError("unauthorized_client");
} }
if (redirect_uri && client.redirect_url !== redirect_uri) { if (redirect_uri && client.redirect_url !== redirect_uri) {
Logging.log(redirect_uri, client.redirect_url); Logging.log(redirect_uri, client.redirect_url);
return res.send( 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!" "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 permissions: IPermission[] = [];
let proms: PromiseLike<void>[] = []; let proms: PromiseLike<void>[] = [];
if (scopes) { if (scopes) {
for (let perm of scopes.filter((e) => e !== "read_user")) { for (let perm of scopes.filter((e) => e !== "read_user")) {
let oid = undefined; let oid = undefined;
try { try {
oid = new ObjectID(perm); oid = new ObjectId(perm);
} catch (err) { } catch (err) {
Logging.error(err); Logging.error(err);
continue; continue;
} }
proms.push( proms.push(
Permission.findById(oid).then((p) => { Permission.findById(oid).then((p) => {
if (!p) return Promise.reject(new Error()); if (!p) return Promise.reject(new Error());
permissions.push(p); permissions.push(p);
}) })
); );
} }
} }
let err = undefined; let err = undefined;
await Promise.all(proms).catch((e) => { await Promise.all(proms).catch((e) => {
err = e; err = e;
}); });
if (err) { if (err) {
Logging.error(err); Logging.error(err);
return sendError("invalid_scope"); return sendError("invalid_scope");
} }
let grant: IGrant | undefined = await Grant.findOne({ let grant: IGrant | undefined = await Grant.findOne({
client: client._id, client: client._id,
user: req.user._id, user: req.user._id,
}); });
Logging.debug("Grant", grant, permissions); Logging.debug("Grant", grant, permissions);
let missing_permissions: IPermission[] = []; let missing_permissions: IPermission[] = [];
if (grant) { if (grant) {
missing_permissions = grant.permissions missing_permissions = grant.permissions
.map((perm) => permissions.find((p) => p._id.equals(perm))) .map((perm) => permissions.find((p) => p._id.equals(perm)))
.filter((e) => !!e); .filter((e) => !!e);
} else { } else {
missing_permissions = permissions; missing_permissions = permissions;
} }
let client_granted_perm = missing_permissions.filter( let client_granted_perm = missing_permissions.filter(
(e) => e.grant_type == "client" (e) => e.grant_type == "client"
); );
if (client_granted_perm.length > 0) { if (client_granted_perm.length > 0) {
return sendError("no_permission"); return sendError("no_permission");
} }
if (!grant && missing_permissions.length > 0) { if (!grant && missing_permissions.length > 0) {
await new Promise<void>((yes, no) => await new Promise<void>((yes, no) =>
GetUserMiddleware(false, true)( GetUserMiddleware(false, true)(
req, req,
res, res,
(err?: Error | string) => (err ? no(err) : yes()) (err?: Error | string) => (err ? no(err) : yes())
) )
); // Maybe unresolved when redirect is happening ); // Maybe unresolved when redirect is happening
if (view) { if (view) {
res.send( res.send(
GetAuthPage( GetAuthPage(
req.__, req.__,
client.name, client.name,
permissions.map((perm) => { permissions.map((perm) => {
return { return {
name: perm.name, name: perm.name,
description: perm.description, description: perm.description,
logo: client.logo, logo: client.logo,
}; };
}) })
) )
); );
return; return;
} else { } else {
if ((req.body.allow = "true")) { if ((req.body.allow = "true")) {
if (!grant) if (!grant)
grant = Grant.new({ grant = Grant.new({
client: client._id, client: client._id,
user: req.user._id, user: req.user._id,
permissions: [], permissions: [],
}); });
grant.permissions.push( grant.permissions.push(
...missing_permissions.map((e) => e._id) ...missing_permissions.map((e) => e._id)
); );
await Grant.save(grant); await Grant.save(grant);
} else { } else {
return sendError("access_denied"); return sendError("access_denied");
} }
} }
} }
let code = ClientCode.new({ let code = ClientCode.new({
user: req.user._id, user: req.user._id,
client: client._id, client: client._id,
permissions: permissions.map((p) => p._id), permissions: permissions.map((p) => p._id),
validTill: moment().add(30, "minutes").toDate(), validTill: moment().add(30, "minutes").toDate(),
code: randomBytes(16).toString("hex"), code: randomBytes(16).toString("hex"),
}); });
await ClientCode.save(code); await ClientCode.save(code);
let redir = let redir =
client.redirect_url === "$local" ? "/code" : client.redirect_url; client.redirect_url === "$local" ? "/code" : client.redirect_url;
let ruri = let ruri =
redir + `?code=${code.code}${state ? "&state=" + state : ""}`; redir + `?code=${code.code}${state ? "&state=" + state : ""}`;
if (nored === "true") { if (nored === "true") {
res.json({ res.json({
redirect_uri: ruri, redirect_uri: ruri,
}); });
} else { } else {
res.redirect(ruri); res.redirect(ruri);
} }
} }
} catch (err) { } catch (err) {
Logging.error(err); Logging.error(err);
sendError("server_error"); sendError("server_error");
} }
}); });
export default GetAuthRoute; export default GetAuthRoute;

View File

@ -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 });
}
);

View File

@ -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 });
}
);

View File

@ -1,132 +1,39 @@
import { Router } from "express"; import { Router } from "express";
import { GetAccount } from "./account"; import Register from "./register";
import { GetContactInfos } from "./contact"; import OAuthRoute from "./oauth";
import Login from "./login";
import Register from "./register"; const UserRoute: Router = Router();
import { DeleteToken, GetToken } from "./token";
import TwoFactorRoute from "./twofactor"; /**
import OAuthRoute from "./oauth"; * @api {post} /user/register
* @apiName UserRegister
const UserRoute: Router = Router(); *
* @apiGroup user
/** * @apiPermission none
* @api {post} /user/register *
* @apiName UserRegister * @apiParam {String} mail EMail linked to this Account
* * @apiParam {String} username The new Username
* @apiGroup user * @apiParam {String} password Password hashed and salted like specification
* @apiPermission none * @apiParam {String} salt The Salt used for password hashing
* * @apiParam {String} regcode The regcode, that should be used
* @apiParam {String} mail EMail linked to this Account * @apiParam {String} gender Gender can be: "male", "female", "other", "none"
* @apiParam {String} username The new Username * @apiParam {String} name The real name of the User
* @apiParam {String} password Password hashed and salted like specification *
* @apiParam {String} salt The Salt used for password hashing * @apiSuccess {Boolean} success
* @apiParam {String} regcode The regcode, that should be used *
* @apiParam {String} gender Gender can be: "male", "female", "other", "none" * @apiErrorExample {Object} Error-Response:
* @apiParam {String} name The real name of the User {
* error: [
* @apiSuccess {Boolean} success {
* message: "Some Error",
* @apiErrorExample {Object} Error-Response: field: "username"
{ }
error: [ ],
{ status: 400
message: "Some Error", }
field: "username" */
} UserRoute.post("/register", Register);
],
status: 400 UserRoute.use("/oauth", OAuthRoute);
}
*/ export default UserRoute;
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;

View File

@ -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;

View File

@ -1,38 +1,38 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import Stacker from "../../middlewares/stacker"; import Stacker from "../../middlewares/stacker";
import { GetUserMiddleware } from "../../middlewares/user"; import { GetUserMiddleware } from "../../middlewares/user";
import { URL } from "url"; import { URL } from "url";
import Client from "../../../models/client"; import Client from "../../../models/client";
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; import RequestError, { HttpStatusCode } from "../../../helper/request_error";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import moment = require("moment"); import moment = require("moment");
import RefreshToken from "../../../models/refresh_token"; import RefreshToken from "../../../models/refresh_token";
import { refreshTokenValidTime } from "../../../config"; import { refreshTokenValidTime } from "../../../config";
import { getClientWithOrigin } from "./_helper"; import { getClientWithOrigin } from "./_helper";
import Permission from "../../../models/permissions"; import Permission from "../../../models/permissions";
export const GetPermissionsForAuthRequest = Stacker( export const GetPermissionsForAuthRequest = Stacker(
GetUserMiddleware(true, false), GetUserMiddleware(true, false),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { client_id, origin, permissions } = req.query as { const { client_id, origin, permissions } = req.query as {
[key: string]: string; [key: string]: string;
}; };
const client = await getClientWithOrigin(client_id, origin); const client = await getClientWithOrigin(client_id, origin);
const perm = permissions.split(",").filter((e) => !!e); const perm = permissions.split(",").filter((e) => !!e);
const resolved = await Promise.all( const resolved = await Promise.all(
perm.map((p) => Permission.findById(p)) perm.map((p) => Permission.findById(p))
); );
if (resolved.some((e) => e.grant_type !== "user")) { if (resolved.some((e) => e.grant_type !== "user")) {
throw new RequestError( throw new RequestError(
"Invalid Permission requested", "Invalid Permission requested",
HttpStatusCode.BAD_REQUEST HttpStatusCode.BAD_REQUEST
); );
} }
res.json({ permissions: resolved }); res.json({ permissions: resolved });
} }
); );

View File

@ -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 });
}
);

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,75 +1,81 @@
import { parse } from "@hibas123/config"; import { parse } from "@hibas123/config";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import moment = require("moment"); import moment = require("moment");
export const refreshTokenValidTime = moment.duration(6, "month"); export const refreshTokenValidTime = moment.duration(6, "month");
dotenv.config(); dotenv.config();
export interface DatabaseConfig { export interface DatabaseConfig {
host: string; host: string;
database: string; database: string;
} }
export interface WebConfig { export interface WebConfig {
port: string; port: string;
secure: "true" | "false" | undefined; secure: "true" | "false" | undefined;
} }
export interface CoreConfig { export interface CoreConfig {
name: string; name: string;
url: string; url: string;
dev: boolean; dev: boolean;
} secret: string;
}
export interface Config {
core: CoreConfig; export interface Config {
database: DatabaseConfig; core: CoreConfig;
web: WebConfig; database: DatabaseConfig;
} web: WebConfig;
}
const config = (parse(
{ const config = (parse(
core: { {
dev: { core: {
default: false, dev: {
type: Boolean, default: false,
}, type: Boolean,
name: { },
type: String, name: {
default: "Open Auth", type: String,
}, default: "Open Auth",
url: String, },
}, url: String,
database: { secret: {
database: { type: String,
type: String, optional: false,
default: "openauth", description: "Cookie secret"
}, }
host: { },
type: String, database: {
default: "localhost", database: {
}, type: String,
}, default: "openauth",
web: { },
port: { host: {
type: Number, type: String,
default: 3004, default: "localhost",
}, },
secure: { },
type: Boolean, web: {
default: false, port: {
}, type: Number,
}, default: 3004,
}, },
"config.ini" secure: {
) as any) as Config; type: Boolean,
default: false,
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! " "config.ini"
); ) as any) as Config;
export default 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;

View File

@ -1,13 +1,13 @@
import SafeMongo from "@hibas123/safe_mongo"; import SafeMongo from "@hibas123/safe_mongo";
import Config from "./config"; import Config from "./config";
let dbname = "openauth"; let dbname = "openauth";
let host = "localhost"; let host = "localhost";
if (Config.database) { if (Config.database) {
if (Config.database.database) dbname = Config.database.database; if (Config.database.database) dbname = Config.database.database;
if (Config.database.host) host = Config.database.host; if (Config.database.host) host = Config.database.host;
} }
if (Config.core.dev) dbname += "_dev"; if (Config.core.dev) dbname += "_dev";
const DB = new SafeMongo("mongodb://" + host, dbname, { const DB = new SafeMongo("mongodb://" + host, dbname, {
useUnifiedTopology: true,
}); });
export default DB; export default DB;

View File

@ -1,16 +1,23 @@
import { IUser } from "./models/user"; import { IUser } from "./models/user";
import { IClient } from "./models/client"; import { IClient } from "./models/client";
import { ILoginToken } from "./models/login_token";
declare module "express" {
declare module "express" { interface Request {
interface Request { user: IUser;
user: IUser; client: IClient;
client: IClient; isAdmin: boolean;
isAdmin: boolean; special: boolean;
special: boolean; }
token: { }
login: ILoginToken;
special?: ILoginToken; declare module 'express-session' {
}; interface SessionData {
} user_id: string;
} validated: boolean;
login_state: {
username: string;
password_correct: boolean;
webauthn_challenge?: any;
};
}
}

View File

@ -1,60 +1,60 @@
import { IUser, Gender } from "../models/user"; import { IUser, Gender } from "../models/user";
import { ObjectID } from "bson"; import { ObjectId } from "bson";
import { createJWT } from "../keys"; import { createJWT } from "../keys";
import { IClient } from "../models/client"; import { IClient } from "../models/client";
import config from "../config"; import config from "../config";
import * as moment from "moment"; import moment = require("moment");
export interface OAuthJWT { export interface OAuthJWT {
user: string; user: string;
username: string; username: string;
permissions: string[]; permissions: string[];
application: string; application: string;
} }
const issuer = config.core.url; const issuer = config.core.url;
export const IDTokenJWTExp = moment.duration(30, "m").asSeconds(); export const IDTokenJWTExp = moment.duration(30, "m").asSeconds();
export function getIDToken(user: IUser, client_id: string, nonce: string) { export function getIDToken(user: IUser, client_id: string, nonce: string) {
return createJWT( return createJWT(
{ {
user: user.uid, user: user.uid,
name: user.name, name: user.name,
nickname: user.username, nickname: user.username,
username: user.username, username: user.username,
preferred_username: user.username, preferred_username: user.username,
gender: Gender[user.gender], gender: Gender[user.gender],
nonce, nonce,
}, },
{ {
expiresIn: IDTokenJWTExp, expiresIn: IDTokenJWTExp,
issuer, issuer,
algorithm: "RS256", algorithm: "RS256",
subject: user.uid, subject: user.uid,
audience: client_id, audience: client_id,
} }
); );
} }
export const AccessTokenJWTExp = moment.duration(6, "h"); export const AccessTokenJWTExp = moment.duration(6, "h");
export function getAccessTokenJWT(token: { export function getAccessTokenJWT(token: {
user: IUser; user: IUser;
permissions: ObjectID[]; permissions: ObjectId[];
client: IClient; client: IClient;
}) { }) {
return createJWT( return createJWT(
<OAuthJWT>{ <OAuthJWT>{
user: token.user.uid, user: token.user.uid,
username: token.user.username, username: token.user.username,
permissions: token.permissions.map((p) => p.toHexString()), permissions: token.permissions.map((p) => p.toHexString()),
application: token.client.client_id, application: token.client.client_id,
}, },
{ {
expiresIn: AccessTokenJWTExp.asSeconds(), expiresIn: AccessTokenJWTExp.asSeconds(),
issuer, issuer,
algorithm: "RS256", algorithm: "RS256",
subject: token.user.uid, subject: token.user.uid,
audience: token.client.client_id, audience: token.client.client_id,
} }
); );
} }

View 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);
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,25 +1,25 @@
import DB from "../database"; import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model"; import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb"; import { ObjectId } from "mongodb";
export interface IGrant extends ModelDataBase { export interface IGrant extends ModelDataBase {
user: ObjectID; user: ObjectId;
client: ObjectID; client: ObjectId;
permissions: ObjectID[]; permissions: ObjectId[];
} }
const Grant = DB.addModel<IGrant>({ const Grant = DB.addModel<IGrant>({
name: "grant", name: "grant",
versions: [ versions: [
{ {
migration: () => {}, migration: () => { },
schema: { schema: {
user: { type: ObjectID }, user: { type: ObjectId },
client: { type: ObjectID }, client: { type: ObjectId },
permissions: { type: ObjectID, array: true }, permissions: { type: ObjectId, array: true },
}, },
}, },
], ],
}); });
export default Grant; export default Grant;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,69 +1,71 @@
import DB from "../database"; import { TFAType } from "@hibas123/openauth-internalapi";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model"; import DB from "../database";
import { ObjectID } from "bson"; import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectId } from "bson";
export enum TFATypes { import { Binary } from "mongodb";
TOTP,
BACKUP_CODE, export { TFAType as TFATypes };
WEBAUTHN,
APP_ALLOW,
} export const TFANames = new Map<TFAType, string>();
TFANames.set(TFAType.TOTP, "Authenticator");
export const TFANames = new Map<TFATypes, string>(); TFANames.set(TFAType.BACKUP_CODE, "Backup Codes");
TFANames.set(TFATypes.TOTP, "Authenticator"); TFANames.set(TFAType.WEBAUTHN, "Security Key (WebAuthn)");
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes"); TFANames.set(TFAType.APP_ALLOW, "App Push");
TFANames.set(TFATypes.WEBAUTHN, "Security Key (WebAuthn)");
TFANames.set(TFATypes.APP_ALLOW, "App Push"); export interface ITwoFactor extends ModelDataBase {
user: ObjectId;
export interface ITwoFactor extends ModelDataBase { valid: boolean;
user: ObjectID; expires?: Date;
valid: boolean; name?: string;
expires?: Date; type: TFAType;
name?: string; data: any;
type: TFATypes; }
data: any;
} export interface ITOTP extends ITwoFactor {
data: string;
export interface IOTC extends ITwoFactor { }
data: string;
} export interface IWebAuthn extends ITwoFactor {
data: {
export interface IYubiKey extends ITwoFactor { challenge?: any;
data: { device?: {
registration?: any; credentialID: Binary;
publicKey: string; credentialPublicKey: Binary;
keyHandle: string; counter: number;
}; transports: AuthenticatorTransport[]
} }
};
export interface IU2F extends ITwoFactor { }
data: {
challenge?: string; export interface IU2F extends ITwoFactor {
publicKey: string; data: {
keyHandle: string; challenge?: string;
registration?: string; publicKey: string;
}; keyHandle: string;
} registration?: string;
};
export interface IBackupCode extends ITwoFactor { }
data: string[];
} export interface IBackupCode extends ITwoFactor {
data: string[];
const TwoFactor = DB.addModel<ITwoFactor>({ }
name: "twofactor",
versions: [ const TwoFactor = DB.addModel<ITwoFactor>({
{ name: "twofactor",
migration: (e) => { }, versions: [
schema: { {
user: { type: ObjectID }, migration: (e) => { },
valid: { type: Boolean }, schema: {
expires: { type: Date, optional: true }, user: { type: ObjectId },
name: { type: String, optional: true }, valid: { type: Boolean },
type: { type: Number }, expires: { type: Date, optional: true },
data: { type: "any" }, name: { type: String, optional: true },
}, type: { type: Number },
}, data: { type: "any" },
], },
}); },
],
export default TwoFactor; });
export default TwoFactor;

View File

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

View File

@ -1,147 +1,170 @@
import User, { Gender } from "./models/user"; import User, { Gender } from "./models/user";
import Client from "./models/client"; import Client from "./models/client";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import RegCode from "./models/regcodes"; import RegCode from "./models/regcodes";
import * as moment from "moment"; import moment from "moment";
import Permission from "./models/permissions"; import Permission from "./models/permissions";
import { ObjectID } from "bson"; import { ObjectId } from "mongodb";
import DB from "./database"; import DB from "./database";
import TwoFactor from "./models/twofactor"; import TwoFactor from "./models/twofactor";
import * as speakeasy from "speakeasy"; import LoginToken from "./models/login_token";
import LoginToken from "./models/login_token"; import Mail from "./models/mail";
import Mail from "./models/mail";
export default async function TestData() {
export default async function TestData() { Logging.warn("Running in dev mode! Database will be cleared!");
Logging.warn("Running in dev mode! Database will be cleared!"); // await DB.db.dropDatabase();
await DB.db.dropDatabase();
let mail = await Mail.findOne({ mail: "test@test.de" });
let mail = await Mail.findOne({ mail: "test@test.de" }); if (!mail) {
if (!mail) { mail = Mail.new({
mail = Mail.new({ mail: "test@test.de",
mail: "test@test.de", primary: true,
primary: true, verified: true,
verified: true, });
});
await Mail.save(mail);
await Mail.save(mail); }
}
let u = await User.findOne({ username: "test" });
let u = await User.findOne({ username: "test" }); if (!u) {
if (!u) { Logging.log("Adding test user");
Logging.log("Adding test user"); u = User.new({
u = User.new({ username: "test",
username: "test", birthday: new Date(),
birthday: new Date(), gender: Gender.male,
gender: Gender.male, name: "Test Test",
name: "Test Test", password:
password: "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc",
"125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc", salt: "test",
salt: "test", admin: true,
admin: true, phones: [
phones: [ { phone: "+4915962855955", primary: true, verified: true },
{ phone: "+4915962855955", primary: true, verified: true }, { phone: "+4915962855932", primary: false, verified: false },
{ phone: "+4915962855932", primary: false, verified: false }, ],
], mails: [mail._id],
mails: [mail._id], });
}); await User.save(u);
await User.save(u); }
}
let c = await Client.findOne({ client_id: "test001" });
let c = await Client.findOne({ client_id: "test001" }); if (!c) {
if (!c) { Logging.log("Adding test client");
Logging.log("Adding test client"); c = Client.new({
c = Client.new({ client_id: "test001",
client_id: "test001", client_secret: "test001",
client_secret: "test001", internal: true,
internal: true, maintainer: u._id,
maintainer: u._id, name: "Test Client",
name: "Test Client", website: "http://example.com",
website: "http://example.com", redirect_url: "http://example.com",
redirect_url: "http://example.com", featured: true,
featured: true, description:
description: "This client is just for testing purposes. It does not have any functionality.",
"This client is just for testing purposes. It does not have any functionality.", });
}); await Client.save(c);
await Client.save(c); }
}
let perm = await Permission.findById("507f1f77bcf86cd799439011");
let perm = await Permission.findById("507f1f77bcf86cd799439011"); if (!perm) {
if (!perm) { Logging.log("Adding test permission");
Logging.log("Adding test permission"); perm = Permission.new({
perm = Permission.new({ _id: new ObjectId("507f1f77bcf86cd799439011"),
_id: new ObjectID("507f1f77bcf86cd799439011"), name: "TestPerm",
name: "TestPerm", description: "Permission just for testing purposes",
description: "Permission just for testing purposes", client: c._id,
client: c._id, });
});
await (await (Permission as any)._collection).insertOne(perm);
await (await (Permission as any)._collection).insertOne(perm);
// Permission.save(perm);
// Permission.save(perm); }
}
let r = await RegCode.findOne({ token: "test" });
let r = await RegCode.findOne({ token: "test" }); if (!r) {
if (!r) { Logging.log("Adding test reg_code");
Logging.log("Adding test reg_code"); r = RegCode.new({
r = RegCode.new({ token: "test",
token: "test", valid: true,
valid: true, validTill: moment().add("1", "year").toDate(),
validTill: moment().add("1", "year").toDate(), });
}); await RegCode.save(r);
await RegCode.save(r); }
}
let t = await TwoFactor.findOne({ user: u._id, type: 0 });
let t = await TwoFactor.findOne({ user: u._id, type: 0 }); if (!t) {
if (!t) { Logging.log("Adding test TOTP")
t = TwoFactor.new({ t = TwoFactor.new({
user: u._id, user: u._id,
name: "Test OTP", name: "Test OTP",
type: 0, type: 0,
valid: true, valid: true,
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ", data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
expires: null, expires: null,
}); });
await TwoFactor.save(t); await TwoFactor.save(t);
} }
let login_token = await LoginToken.findOne({ token: "test01" }); // let tw = await TwoFactor.findOne({ user: u._id, type: 2 });
if (login_token) await LoginToken.delete(login_token); // if (!tw) {
// Logging.log("Adding test WebAuthn")
login_token = LoginToken.new({ // tw = TwoFactor.new({
browser: "DEMO", // user: u._id,
ip: "10.0.0.1", // name: "WebAuthn",
special: false, // type: 2,
token: "test01", // valid: true,
valid: true, // data: {
validTill: moment().add("10", "years").toDate(), // device: {
user: u._id, // credentialPublicKey: Buffer.from("pQECAyYgASFYINiHCRopJIn1GoTXq7SpDTJR1nzocqOWhjvpYaKLzzhSIlggvuHhjABe8NxbOIGA11vrd5deUT5R30anpE7W7xzPcsk=", "base64"),
validated: true, // credentialID: Buffer.from("i/BJiffx0bxjQ9Ptyvc9ORELXALxrvD6pad1Xc/2nDI=", "base64"),
}); // counter: 1,
await LoginToken.save(login_token); // transports: [
// "usb"
let special_token = await LoginToken.findOne({ token: "test02" }); // ]
if (special_token) await LoginToken.delete(special_token); // }
// }
special_token = LoginToken.new({ // });
browser: "DEMO", // await TwoFactor.save(tw);
ip: "10.0.0.1", // }
special: true,
token: "test02", let login_token = await LoginToken.findOne({ token: "test01" });
valid: true, if (login_token) await LoginToken.delete(login_token);
validTill: moment().add("10", "years").toDate(),
user: u._id, login_token = LoginToken.new({
validated: true, browser: "DEMO",
}); ip: "10.0.0.1",
await LoginToken.save(special_token); special: false,
token: "test01",
// setInterval(() => { valid: true,
// let code = speakeasy.totp({ validTill: moment().add("10", "years").toDate(),
// secret: t.data, user: u._id,
// encoding: "base32" validated: true,
// }) });
// Logging.debug("OTC Code is:", code); await LoginToken.save(login_token);
// }, 1000)
let special_token = await LoginToken.findOne({ token: "test02" });
console.log("Finished adding test data") 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");
}

View File

@ -1,121 +1,121 @@
import { import {
IRouter, IRouter,
Request, Request,
RequestHandler, RequestHandler,
Router, Router,
static as ServeStatic, static as ServeStatic,
} from "express"; } from "express";
import * as Handlebars from "handlebars"; import * as Handlebars from "handlebars";
import * as moment from "moment"; import moment = require("moment");
import { GetUserMiddleware, UserMiddleware } from "../api/middlewares/user"; import { GetUserMiddleware, UserMiddleware } from "../api/middlewares/user";
import GetAuthRoute from "../api/oauth/auth"; import GetAuthRoute from "../api/oauth/auth";
import config from "../config"; import config from "../config";
import { HttpStatusCode } from "../helper/request_error"; import { HttpStatusCode } from "../helper/request_error";
import GetAdminPage from "./admin"; import GetAdminPage from "./admin";
import GetRegistrationPage from "./register"; import GetRegistrationPage from "./register";
import * as path from "path"; import * as path from "path";
const viewsv2_location = path.join(path.dirname(require.resolve("@hibas123/openauth-views-v2")), "build"); const viewsv2_location = path.join(path.dirname(require.resolve("@hibas123/openauth-views-v2")), "build");
Handlebars.registerHelper("appname", () => config.core.name); Handlebars.registerHelper("appname", () => config.core.name);
const cacheTime = !config.core.dev const cacheTime = !config.core.dev
? moment.duration(1, "month").asSeconds() ? moment.duration(1, "month").asSeconds()
: 1000; : 1000;
const addCache: RequestHandler = (req, res, next) => { const addCache: RequestHandler = (req, res, next) => {
res.setHeader("cache-control", "public, max-age=" + cacheTime); res.setHeader("cache-control", "public, max-age=" + cacheTime);
next(); next();
}; };
const ViewRouter: IRouter = Router(); const ViewRouter: IRouter = Router();
ViewRouter.get("/", UserMiddleware, (req, res) => { ViewRouter.get("/", UserMiddleware, (req, res) => {
res.send("This is the main page"); res.send("This is the main page");
}); });
ViewRouter.get("/register", (req, res) => { ViewRouter.get("/register", (req, res) => {
res.setHeader("Cache-Control", "public, max-age=" + cacheTime); res.setHeader("Cache-Control", "public, max-age=" + cacheTime);
res.send(GetRegistrationPage(req.__)); res.send(GetRegistrationPage(req.__));
}); });
ViewRouter.use( ViewRouter.use(
"/login", "/login",
addCache, addCache,
ServeStatic(path.join(viewsv2_location, "login"), { cacheControl: false }) ServeStatic(path.join(viewsv2_location, "login"), { cacheControl: false })
); );
ViewRouter.use( ViewRouter.use(
"/user", "/user",
addCache, addCache,
ServeStatic(path.join(viewsv2_location, "user"), { cacheControl: false, }) ServeStatic(path.join(viewsv2_location, "user"), { cacheControl: false, })
); );
ViewRouter.use( ViewRouter.use(
"/static", "/static",
addCache, addCache,
ServeStatic(path.join(viewsv2_location, "../static"), { cacheControl: false, }) ServeStatic(path.join(viewsv2_location, "../static"), { cacheControl: false, })
); );
ViewRouter.get("/code", (req, res) => { ViewRouter.get("/code", (req, res) => {
res.setHeader("Cache-Control", "no-cache"); res.setHeader("Cache-Control", "no-cache");
if (req.query.error) res.send("Some error occured: " + req.query.error); if (req.query.error) res.send("Some error occured: " + req.query.error);
else res.send(`Your code is: ${req.query.code}`); else res.send(`Your code is: ${req.query.code}`);
}); });
ViewRouter.get( ViewRouter.get(
"/admin", "/admin",
GetUserMiddleware(false, true), GetUserMiddleware(false, true),
(req: Request, res, next) => { (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN); if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN);
else next(); else next();
}, },
(req, res) => { (req, res) => {
res.send(GetAdminPage(req.__)); res.send(GetAdminPage(req.__));
} }
); );
ViewRouter.get("/auth", GetAuthRoute(true)); ViewRouter.get("/auth", GetAuthRoute(true));
ViewRouter.use( ViewRouter.use(
"/popup", "/popup",
GetUserMiddleware(false, false), GetUserMiddleware(false, false),
addCache, addCache,
ServeStatic(path.join(viewsv2_location, "popup"), { cacheControl: false }) ServeStatic(path.join(viewsv2_location, "popup"), { cacheControl: false })
); );
// ViewRouter.get("/popup", UserMiddleware, (req, res) => { // ViewRouter.get("/popup", UserMiddleware, (req, res) => {
// res.send(GetPopupPage(req.__)); // res.send(GetPopupPage(req.__));
// }); // });
// if (config.core.dev) { // if (config.core.dev) {
// const logo = // 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=="; // "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) => { // ViewRouter.get("/devauth", (req, res) => {
// res.send( // res.send(
// GetAuthPage(req.__, "Test 05265", [ // GetAuthPage(req.__, "Test 05265", [
// { // {
// name: "Access Profile", // name: "Access Profile",
// description: // 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.", // "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, // logo: logo,
// }, // },
// { // {
// name: "Test 1", // name: "Test 1",
// description: // description:
// "This is not an real permission. This is used just to verify the layout", // "This is not an real permission. This is used just to verify the layout",
// logo: logo, // logo: logo,
// }, // },
// { // {
// name: "Test 2", // name: "Test 2",
// description: // description:
// "This is not an real permission. This is used just to verify the layout", // "This is not an real permission. This is used just to verify the layout",
// logo: logo, // logo: logo,
// }, // },
// ]) // ])
// ); // );
// }); // });
// } // }
export default ViewRouter; export default ViewRouter;

View File

@ -1,122 +1,175 @@
import { WebConfig } from "./config"; import config, { WebConfig } from "./config";
import * as express from "express"; import express from "express";
import { Express } from "express"; import { Express } from "express";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import { Format } from "@hibas123/logging";
import * as bodyparser from "body-parser";
import * as cookieparser from "cookie-parser"; import bodyparser from "body-parser";
import cookieparser from "cookie-parser";
import * as i18n from "i18n"; import session from "express-session";
import * as compression from "compression"; import MongoStore from "connect-mongo";
import ApiRouter from "./api";
import ViewRouter from "./views/views"; import i18n from "i18n";
import RequestError, { HttpStatusCode } from "./helper/request_error"; import compression from "compression";
import ApiRouter from "./api";
export default class Web { import ViewRouter from "./views";
server: Express; import RequestError, { HttpStatusCode } from "./helper/request_error";
private port: number; import DB from "./database";
import promiseMiddleware from "./helper/promiseMiddleware";
constructor(config: WebConfig) { import User from "./models/user";
this.server = express(); import LoginToken, { CheckToken } from "./models/login_token";
this.port = Number(config.port);
this.registerMiddleware(); export default class Web {
this.registerEndpoints(); server: Express;
this.registerErrorHandler(); private port: number;
}
constructor(config: WebConfig) {
listen() { this.server = express();
this.server.listen(this.port, () => { this.port = Number(config.port);
Logging.log(`Server listening on port ${this.port}`); this.registerMiddleware();
}); this.registerUserSession();
} this.registerEndpoints();
this.registerErrorHandler();
private registerMiddleware() { }
this.server.use(cookieparser());
this.server.use( listen() {
bodyparser.json(), this.server.listen(this.port, () => {
bodyparser.urlencoded({ extended: true }) Logging.log(`Server listening on port ${this.port}`);
); });
this.server.use(i18n.init); }
//Logging Middleware private registerMiddleware() {
this.server.use((req, res, next) => { this.server.use(session({
let start = process.hrtime(); secret: config.core.secret,
let finished = false; resave: false,
let to = false; saveUninitialized: false,
let listener = () => { store: MongoStore.create({
if (finished) return; client: DB.getClient(),
finished = true; dbName: DB.db.databaseName,
let td = process.hrtime(start); collectionName: "sessions",
let time = !to ? (td[0] * 1e3 + td[1] / 1e6).toFixed(2) : "--.--"; autoRemove: "native",
let resColor = ""; touchAfter: 60 * 60 * 24,
if (res.statusCode >= 200 && res.statusCode < 300) }),
resColor = "\x1b[32m"; cookie: {
//Green maxAge: 1000 * 60 * 60 * 24 * 30 * 6,
else if (res.statusCode === 304 || res.statusCode === 302) secure: !config.core.dev,
resColor = "\x1b[33m"; sameSite: "strict",
else if (res.statusCode >= 400 && res.statusCode < 500) }
resColor = "\x1b[36m"; }))
//Cyan this.server.use(cookieparser());
else if (res.statusCode >= 500 && res.statusCode < 600) this.server.use(
resColor = "\x1b[31m"; //Red bodyparser.json(),
let m = req.method; bodyparser.urlencoded({ extended: true })
while (m.length < 4) m += " "; );
Logging.log( this.server.use(i18n.init);
`${m} ${req.originalUrl} ${(req as any).language || ""
} ${resColor}${res.statusCode}\x1b[0m - ${time}ms` //Logging Middleware
); this.server.use((req, res, next) => {
res.removeListener("finish", listener); let start = process.hrtime();
}; let finished = false;
res.on("finish", listener); let to = false;
setTimeout(() => { let listener = () => {
to = true; if (finished) return;
listener(); finished = true;
}, 2000); let td = process.hrtime(start);
next(); 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)
this.server.use( resFormat = Format.green;
compression({ //Green
filter: (req, res) => { else if (res.statusCode === 304 || res.statusCode === 302)
if (req.headers["x-no-compression"]) { resFormat = Format.yellow; //"\x1b[33m";
return false; else if (res.statusCode >= 400 && res.statusCode < 500)
} resFormat = Format.red; // "\x1b[36m";
return compression.filter(req, res); //Cyan
}, else if (res.statusCode >= 500 && res.statusCode < 600)
}) resFormat = Format.cyan //"\x1b[31m"; //Red
);
} let m = req.method;
while (m.length < 4) m += " ";
private registerEndpoints() { Logging.getChild("HTTP").log(
this.server.use("/api", ApiRouter); `${m} ${req.originalUrl} ${(req as any).language || ""
this.server.use("/", ViewRouter); }`, resFormat(res.statusCode), `- ${time}ms`
} );
res.removeListener("finish", listener);
private registerErrorHandler() { };
this.server.use((error, req: express.Request, res, next) => { res.on("finish", listener);
if (!(error instanceof RequestError)) { setTimeout(() => {
error = new RequestError( to = true;
error.message, listener();
error.status || HttpStatusCode.INTERNAL_SERVER_ERROR, }, 2000);
error.nolog || false next();
); });
}
this.server.use(
if (error.status === 500 && !(<any>error).nolog) { compression({
Logging.error(error); filter: (req, res) => {
} else { if (req.headers["x-no-compression"]) {
Logging.log("Responded with Error", error.status, error.message); return false;
} }
return compression.filter(req, res);
if (req.accepts(["json"])) { },
res.json_status = error.status || 500; })
res.json({ );
error: error.message, }
status: error.status || 500,
additional: error.additional, private registerEndpoints() {
}); this.server.use("/api", ApiRouter);
} else res.status(error.status || 500).send(error.message); 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();
}));
}
}

View File

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

View File

@ -11,9 +11,9 @@
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"cssnano": "^6.0.0", "cssnano": "^6.0.0",
"esbuild": "^0.17.15", "esbuild": "^0.17.16",
"flowbite": "^1.6.4", "flowbite": "^1.6.5",
"flowbite-svelte": "^0.34.7", "flowbite-svelte": "^0.34.9",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"postcss-import": "^15.1.0", "postcss-import": "^15.1.0",
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
@ -27,7 +27,7 @@
"svelte": "^3.58.0", "svelte": "^3.58.0",
"svelte-preprocess": "^5.0.3", "svelte-preprocess": "^5.0.3",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
"typescript": "^5.0.3" "typescript": "^5.0.4"
}, },
"scripts": { "scripts": {
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
@ -39,7 +39,9 @@
"@hibas123/theme": "^2.0.6", "@hibas123/theme": "^2.0.6",
"@hibas123/utils": "^2.2.18", "@hibas123/utils": "^2.2.18",
"@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-commonjs": "^24.0.1",
"@simplewebauthn/browser": "^7.2.0",
"cleave.js": "^1.6.0", "cleave.js": "^1.6.0",
"joi": "^17.9.1",
"what-the-pack": "^2.0.3" "what-the-pack": "^2.0.3"
} }
} }

View File

@ -4,8 +4,33 @@
export let title: string; export let title: string;
export let loading = false; export let loading = false;
export let hide = false; export let hide = false;
$: console.log({ loading });
</script> </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> <style>
.wrapper { .wrapper {
min-height: 100vh; min-height: 100vh;
@ -21,6 +46,7 @@
border-radius: 4px; border-radius: 4px;
position: relative; position: relative;
padding-top: 2.5rem; padding-top: 2.5rem;
width: 25rem;
min-height: calc(100px + 2.5rem); min-height: calc(100px + 2.5rem);
min-width: 100px; min-width: 100px;
@ -34,6 +60,7 @@
background-color: var(--primary); background-color: var(--primary);
color: white; color: white;
border-radius: 4px; border-radius: 4px;
text-align: center;
/* padding: 5px 20px; */ /* padding: 5px 20px; */
} }
@ -65,26 +92,3 @@
z-index: 2; z-index: 2;
} }
</style> </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>

View 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>

View File

@ -28,6 +28,7 @@ body {
.group { .group {
position: relative; position: relative;
margin-top: 2rem;
margin-bottom: 24px; margin-bottom: 24px;
min-height: 45px; min-height: 45px;
} }
@ -212,6 +213,11 @@ body {
transition: width 0.2s ease-out, padding-top 0.2s ease-out; transition: width 0.2s ease-out, padding-top 0.2s ease-out;
} }
.btn-wide {
width: 100%;
margin: 0;
}
.loader_box { .loader_box {
width: 64px; width: 64px;
height: 64px; height: 64px;

View File

@ -2,22 +2,38 @@ import { Client } from "@hibas123/openauth-internalapi";
import request, { RequestError } from "./request"; import request, { RequestError } from "./request";
const provider = new Client.ServiceProvider((data) => { const provider = new Client.ServiceProvider((data) => {
request("/api/jrpc", {}, "POST", data, true, true).then(result => { fetch("/api/jrpc", {
provider.onPacket(result); 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 => { }).catch(err => {
if (err instanceof RequestError) { provider.onPacket({
let data = err.response; jsonrpc: "2.0",
if (data.error && Array.isArray(data.error)) { method: data.method,
data.error = data.error[0]; id: data.id,
} error: {
provider.onPacket(data); code: -32603,
} message: err.message,
}); },
})
})
}); });
const InternalAPI = { const InternalAPI = {
Account: new Client.AccountService(provider), Account: new Client.AccountService(provider),
Security: new Client.SecurityService(provider), Security: new Client.SecurityService(provider),
TwoFactor: new Client.TFAService(provider),
Login: new Client.LoginService(provider),
} }
export default InternalAPI; export default InternalAPI;
(window as any).InternalAPI = InternalAPI;

View File

@ -15,17 +15,6 @@
</p> </p>
<h2>Applications using OpenAuth</h2> <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> </div>
<style> <style>

View File

@ -1,124 +1,34 @@
<script> <script lang="ts">
import {} from "flowbite-svelte";
import { LoginState } from "@hibas123/openauth-internalapi";
import Theme from "../../components/theme"; import Theme from "../../components/theme";
import loginState from "./state";
import HoveringContentBox from "../../components/HoveringContentBox.svelte"; import HoveringContentBox from "../../components/HoveringContentBox.svelte";
import Api from "./api.ts"; import { onMount } from "svelte";
import Credentials from "./Credentials.svelte"; import Username from "./Username.svelte";
import Redirect from "./Redirect.svelte"; import Password from "./Password.svelte";
import Twofactor from "./Twofactor.svelte"; import Success from "./Success.svelte";
import TwoFactor from "./TwoFactor.svelte";
const appname = "OpenAuth"; const { state } = loginState;
const states = {
credentials: 1,
twofactor: 3,
redirect: 4,
};
let username = Api.getUsername();
let password = "";
let loading = false;
let state = states.credentials;
function getButtonText(state) {
switch (state) {
case states.username:
return "Next";
case states.password:
return "Login";
default:
return "";
}
}
$: btnText = getButtonText(state);
let error;
// window.addEventListener("popstate", () => {
// state = history.state;
// })
function LoadRedirect() {
state = states.redirect;
}
function Loading() {
state = states.loading;
}
let salt;
async function buttonClick() {
if (state === states.username) {
Loading();
let res = await Api.setUsername(username);
if (res.error) {
error = res.error;
LoadUsername();
} else {
LoadPassword();
}
} else if (state === states.password) {
Loading();
let res = await Api.setPassword(password);
if (res.error) {
error = res.error;
LoadPassword();
} else {
if (res.tfa) {
// TODO: Make TwoFactor UI/-s
} else {
LoadRedirect();
}
}
btnText = "Error";
}
}
function startRedirect() {
state = states.redirect;
// Show message to User and then redirect
setTimeout(() => Api.finish(), 2000);
}
function afterCredentials() {
Object.keys(Api); // Some weird bug needs this???
if (Api.twofactor) {
state = states.twofactor;
} else {
startRedirect();
}
}
function afterTwoFactor() {
startRedirect();
}
</script> </script>
<style>
footer {
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
</style>
<Theme> <Theme>
<HoveringContentBox title="Login" {loading}> <HoveringContentBox title="Login" loading={$state.loading}>
<form action="JavaScript:void(0)"> <form action="JavaScript:void(0)">
{#if state === states.redirect} {#if $state.success}
<Redirect /> <Success />
{:else if state === states.credentials} {:else if !$state.username}
<Credentials next={afterCredentials} setLoading={(s) => (loading = s)} /> <Username on:username={(evt) => loginState.setUsername(evt.detail)} />
{:else if state === states.twofactor} {:else if !$state.password}
<Twofactor finish={afterTwoFactor} setLoading={(s) => (loading = s)} /> <Password
username={$state.username}
on:password={(evt) => loginState.setPassword(evt.detail)}
/>
{:else if $state.requireTwoFactor.length > 0}
<TwoFactor />
{/if} {/if}
</form> </form>
</HoveringContentBox> </HoveringContentBox>
<footer>
<p>Powered by {appname}</p>
</footer>
</Theme> </Theme>

View File

@ -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>

View 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>

View 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
>

View File

@ -1,8 +1,8 @@
<script> <script>
import Cleave from "cleave.js"; import Cleave from "cleave.js";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Error from "../Error.svelte";
export let error;
// export let label; // export let label;
export let value; export let value;
export let length = 6; export let length = 6;
@ -17,17 +17,11 @@
}); });
</script> </script>
<style>
.error {
color: var(--error);
margin-top: 4px;
}
</style>
<div class="floating group"> <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="highlight" />
<span class="bar" /> <span class="bar" />
<label for="noasidhglk">Code</label> <label for="code-input">Code</label>
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
<Error />
</div> </div>

View 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>

View 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 />

View 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>

View File

@ -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>

View 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
>

View File

@ -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);
}
}

View 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;

View File

@ -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>

View File

@ -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} />

View File

@ -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} />

View File

@ -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>

View File

@ -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} />

View File

@ -84,9 +84,25 @@ async function onMessage(msg: MessageEvent<any>) {
const url = new URL(msg.origin); const url = new URL(msg.origin);
setAppName(url.hostname); setAppName(url.hostname);
if (!msg.data.client_id) {
alert("The site requesting the login is not valid");
window.close();
return;
}
try { try {
if (!msg.data.type || msg.data.type === "jwt") { if (!msg.data.type || msg.data.type === "jwt") {
console.log("JWT Request"); 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) => { await new Promise<void>((yes) => {
console.log("Await user acceptance"); console.log("Await user acceptance");
setLoading(false); setLoading(false);

View File

@ -1,13 +1,29 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import MainNavbar from "../../components/MainNavbar.svelte";
import Sidebar from "./Sidebar.svelte"; import Sidebar from "./Sidebar.svelte";
import { CurrentPage } from "./nav"; import { CurrentPage } from "./nav";
import PersonalInfo from "./pages/PersonalInfo.svelte"; import PersonalInfo from "./pages/PersonalInfo.svelte";
import Security from "./pages/Security.svelte"; import Security from "./pages/Security.svelte";
let sidebarOpen = false;
let sidebarOpenVisible = false;
onMount(() => {
const unsub = CurrentPage.subscribe(() => {
sidebarOpen = false;
});
return unsub;
});
</script> </script>
<div class="grid main-grid min-h-screen overflow-hidden"> <div class="grid main-grid min-h-screen overflow-hidden">
<div class="col-span-2">
<MainNavbar bind:sidebarOpen bind:sidebarOpenVisible />
</div>
<div> <div>
<Sidebar /> <Sidebar bind:sidebarOpen bind:sidebarOpenVisible />
</div> </div>
<div class="overflow-auto p-4"> <div class="overflow-auto p-4">
{#if $CurrentPage == "personal-info"} {#if $CurrentPage == "personal-info"}
@ -21,5 +37,6 @@
<style> <style>
.main-grid { .main-grid {
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
} }
</style> </style>

View File

@ -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>

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { import {
Sidebar, Sidebar,
SidebarGroup, SidebarGroup,
@ -6,9 +6,29 @@
SidebarWrapper, SidebarWrapper,
} from "flowbite-svelte"; } from "flowbite-svelte";
import { CurrentPage } from "./nav"; 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> </script>
<Sidebar class="h-screen"> <Sidebar class="h-screen" style={open ? "display: block" : "display: none"}>
<SidebarWrapper class="h-full"> <SidebarWrapper class="h-full">
<SidebarGroup> <SidebarGroup>
<SidebarItem <SidebarItem

View 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>

View 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>

View File

@ -1,12 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import { Session, TFAOption, TFAType } from "@hibas123/openauth-internalapi";
type ContactInfo,
type Account,
Gender,
Token,
TwoFactor,
TFAType,
} from "@hibas123/openauth-internalapi";
import InternalAPI from "../../../helper/api"; import InternalAPI from "../../../helper/api";
import Loading from "../Loading.svelte"; import Loading from "../Loading.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
@ -27,18 +20,20 @@
TableBodyCell, TableBodyCell,
Accordion, Accordion,
AccordionItem, AccordionItem,
Alert,
} from "flowbite-svelte"; } from "flowbite-svelte";
import AddTwoFactor from "./AddTwoFactor.svelte";
let tokens: Token[]; let tokens: Session[];
let twofactors: TwoFactor[]; let twofactors: TFAOption[];
let error: string | undefined; let error: string | undefined;
let loading = true; let loading = true;
async function load() { async function load() {
loading = true; loading = true;
try { try {
tokens = await InternalAPI.Security.GetTokens(); tokens = await InternalAPI.Security.GetSessions();
twofactors = await InternalAPI.Security.GetTwofactorOptions(); twofactors = await InternalAPI.TwoFactor.GetOptions();
} catch (e) { } catch (e) {
error = e.message; error = e.message;
} finally { } 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(() => { onMount(() => {
load(); load();
}); });
async function revokeToken(id: string) { async function revokeToken(id: string) {
try { try {
await InternalAPI.Security.RevokeToken(id); await InternalAPI.Security.RevokeSession(id);
await load(); await load();
} catch (e) { } catch (e) {
error = e.message; error = e.message;
@ -65,6 +69,49 @@
[TFAType.BACKUP_CODE]: "Backup-Code", [TFAType.BACKUP_CODE]: "Backup-Code",
[TFAType.APP_ALLOW]: "App-Auth", [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> </script>
<Loading {loading} {error}> <Loading {loading} {error}>
@ -107,19 +154,31 @@
<Heading tag="h5">Change Password</Heading> <Heading tag="h5">Change Password</Heading>
<hr class="mb-6" /> <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"> <div class="mb-6">
<Label for="oldPassword">Old Password</Label> <Label for="oldPassword">Old Password</Label>
<Input type="password" id="oldPassword" /> <Input type="password" id="oldPassword" bind:value={old_pw} />
</div> </div>
<div class="mb-6"> <div class="mb-6">
<Label for="newPassword">New Password</Label> <Label for="newPassword">New Password</Label>
<Input type="password" id="newPassword" /> <Input type="password" id="newPassword" bind:value={new_pw} />
</div> </div>
<div class="mb-6"> <div class="mb-6">
<Label for="newPasswordRepeat">Repeat New Password</Label> <Label for="newPasswordRepeat">Repeat New Password</Label>
<Input type="password" id="newPasswordRepeat" /> <Input
type="password"
id="newPasswordRepeat"
bind:value={new_pw_repeat}
/>
</div> </div>
<Button class="mt-4">Change Password</Button> <Button class="mt-4" on:click={changePassword}>Change Password</Button>
</Card> </Card>
<Card size="xl" class="mt-4"> <Card size="xl" class="mt-4">
@ -130,12 +189,21 @@
{#each twofactors as tfa} {#each twofactors as tfa}
<AccordionItem> <AccordionItem>
<span slot="header">{tfa.name ?? typeToName[tfa.tfatype]}</span> <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> </AccordionItem>
{/each} {/each}
</Accordion> </Accordion>
<Button class="mt-4">Add Option</Button> <Button class="mt-4" on:click={openAddTwoFactor}>Add Option</Button>
</Card> </Card>
<AddTwoFactor on:reload={reload} bind:open={addTwoFactorOpen} />
<!-- <Card size="xl" class="mt-4"> <!-- <Card size="xl" class="mt-4">
<Heading tag="h5">Delete Account</Heading> <Heading tag="h5">Delete Account</Heading>
<hr class="mb-6" /> <hr class="mb-6" />

View 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}

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,6 +0,0 @@
import "../../components/theme";
import App from "./App.svelte";
new App({
target: document.body,
});

View File

@ -23,6 +23,6 @@
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-typescript2": "^0.34.1", "rollup-plugin-typescript2": "^0.34.1",
"sass": "^1.61.0", "sass": "^1.61.0",
"typescript": "^5.0.3" "typescript": "^5.0.4"
} }
} }

16
InternalAPI/account.jrpc Normal file
View 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;
}

View File

@ -1,77 +1,5 @@
type UserRegisterInfo { import "./types";
username: string; import "./twofactor";
name: string; import "./login";
gender: string; import "./account";
mail: string; import "./security";
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[];
}

21
InternalAPI/login.jrpc Normal file
View 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
View 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;
}

View 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
View 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[];
}

View File

@ -14,6 +14,6 @@
"author": "Fabian Stamm <Fabian.Stamm@polizei.hessen.de>", "author": "Fabian Stamm <Fabian.Stamm@polizei.hessen.de>",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"typescript": "^5.0.2" "typescript": "^5.0.4"
} }
} }

View File

@ -4,7 +4,7 @@
"author": "Fabian Stamm <dev@fabianstamm.de>", "author": "Fabian Stamm <dev@fabianstamm.de>",
"private": true, "private": true,
"scripts": { "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-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-backend": "yarn workspace @hibas123/openauth-backend run build",
"build-views-1": "yarn workspace @hibas123/openauth-views-v1 run build", "build-views-1": "yarn workspace @hibas123/openauth-views-v1 run build",

943
yarn.lock

File diff suppressed because it is too large Load Diff