Merge branch 'new-ui-and-api'
This commit is contained in:
commit
3718a1d55c
@ -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
16
.vscode/launch.json
vendored
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "pwa-node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Launch Program",
|
|
||||||
"program": "${workspaceFolder}/lib/index.js",
|
|
||||||
"outFiles": ["${workspaceFolder}/**/*.js"],
|
|
||||||
"preLaunchTask": "build"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
22
.vscode/tasks.json
vendored
22
.vscode/tasks.json
vendored
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
// Unter https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// finden Sie Informationen zum Format von "tasks.json"
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "npm",
|
|
||||||
"script": "build-ts",
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": ["$tsc"],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "never",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"label": "build"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ database=openauth
|
|||||||
|
|
||||||
[core]
|
[core]
|
||||||
name = OpenAuthService
|
name = OpenAuthService
|
||||||
|
secret = dev
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
port = 3000
|
port = 3000
|
||||||
|
@ -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"
|
||||||
}
|
}
|
@ -25,10 +25,11 @@
|
|||||||
"@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/express-session": "^1.17.7",
|
||||||
"@types/i18n": "^0.13.6",
|
"@types/i18n": "^0.13.6",
|
||||||
"@types/ini": "^1.3.31",
|
"@types/ini": "^1.3.31",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/mongodb": "^3.6.20",
|
"@types/mongodb": "^4.0.7",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@types/node-rsa": "^1.1.1",
|
"@types/node-rsa": "^1.1.1",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
@ -39,26 +40,31 @@
|
|||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.3"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hibas123/config": "^1.1.2",
|
"@hibas123/config": "^1.1.2",
|
||||||
"@hibas123/nodelogging": "^3.1.3",
|
"@hibas123/nodelogging": "^3.1.3",
|
||||||
"@hibas123/nodeloggingserver_client": "^1.1.2",
|
"@hibas123/nodeloggingserver_client": "^1.1.2",
|
||||||
|
"@hibas123/openauth-internalapi": "workspace:^",
|
||||||
"@hibas123/openauth-views-v1": "workspace:^",
|
"@hibas123/openauth-views-v1": "workspace:^",
|
||||||
"@hibas123/safe_mongo": "^1.7.1",
|
"@hibas123/safe_mongo": "^2.0.1",
|
||||||
|
"@simplewebauthn/server": "^7.2.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
|
"connect-mongo": "^5.0.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"i18n": "^0.15.1",
|
"i18n": "^0.15.1",
|
||||||
"ini": "^4.0.0",
|
"ini": "^4.0.0",
|
||||||
|
"joi": "^17.9.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongodb": "^3.7.3",
|
"mongodb": "^5.2.0",
|
||||||
"node-rsa": "^1.1.1",
|
"node-rsa": "^1.1.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
|
@ -5,7 +5,7 @@ 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("/")
|
||||||
@ -28,7 +28,7 @@ PermissionRoute.route("/")
|
|||||||
promiseMiddleware(async (req, res) => {
|
promiseMiddleware(async (req, res) => {
|
||||||
let query = {};
|
let query = {};
|
||||||
if (req.query.client) {
|
if (req.query.client) {
|
||||||
query = { client: new ObjectID(req.query.client 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);
|
||||||
|
@ -9,7 +9,7 @@ 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),
|
||||||
@ -22,7 +22,7 @@ export const GetPermissions = Stacker(
|
|||||||
if (user) {
|
if (user) {
|
||||||
const grant = await Grant.findOne({
|
const grant = await Grant.findOne({
|
||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
user: new ObjectID(user),
|
user: new ObjectId(user),
|
||||||
});
|
});
|
||||||
|
|
||||||
permissions = await Promise.all(
|
permissions = await Promise.all(
|
||||||
@ -43,7 +43,7 @@ export const GetPermissions = Stacker(
|
|||||||
if (permission) {
|
if (permission) {
|
||||||
const grants = await Grant.find({
|
const grants = await Grant.find({
|
||||||
client: req.client._id,
|
client: req.client._id,
|
||||||
permissions: new ObjectID(permission),
|
permissions: new ObjectId(permission),
|
||||||
});
|
});
|
||||||
|
|
||||||
users = grants.map((grant) => grant.user.toHexString());
|
users = grants.map((grant) => grant.user.toHexString());
|
||||||
|
@ -2,11 +2,11 @@ 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 * as cors from "cors";
|
import cors from "cors";
|
||||||
import OAuthRoute from "./oauth";
|
import OAuthRoute from "./oauth";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
|
import JRPCEndpoint from "./jrpc";
|
||||||
|
|
||||||
const ApiRouter: express.IRouter = express.Router();
|
const ApiRouter: express.IRouter = express.Router();
|
||||||
ApiRouter.use("/admin", AdminRoute);
|
ApiRouter.use("/admin", AdminRoute);
|
||||||
@ -17,11 +17,28 @@ ApiRouter.use("/oauth", OAuthRoute);
|
|||||||
|
|
||||||
ApiRouter.use("/client", ClientRouter);
|
ApiRouter.use("/client", ClientRouter);
|
||||||
|
|
||||||
// Legacy reasons (deprecated)
|
/**
|
||||||
ApiRouter.use("/", ClientRouter);
|
* @api {post} /jrpc
|
||||||
|
* @apiName InternalJRPCEndpoint
|
||||||
|
*
|
||||||
|
* @apiGroup user
|
||||||
|
* @apiPermission none
|
||||||
|
*
|
||||||
|
* @apiErrorExample {Object} Error-Response:
|
||||||
|
{
|
||||||
|
error: [
|
||||||
|
{
|
||||||
|
message: "Some Error",
|
||||||
|
field: "username"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
status: 400
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
ApiRouter.post("/jrpc", JRPCEndpoint);
|
||||||
|
|
||||||
// Legacy reasons (deprecated)
|
// Legacy reasons (deprecated)
|
||||||
ApiRouter.post("/login", Login);
|
ApiRouter.use("/", ClientRouter);
|
||||||
|
|
||||||
ApiRouter.get("/config.json", (req, res) => {
|
ApiRouter.get("/config.json", (req, res) => {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
@ -18,7 +18,7 @@ export const OAuthInternalApp = Stacker(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@ -29,13 +29,11 @@ export const OAuthInternalApp = Stacker(
|
|||||||
});
|
});
|
||||||
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();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
38
Backend/src/api/jrpc/index.ts
Normal file
38
Backend/src/api/jrpc/index.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Format } from "@hibas123/logging";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
import { Server, } from "@hibas123/openauth-internalapi";
|
||||||
|
import { RequestObject, ResponseObject } from "@hibas123/openauth-internalapi/lib/service_base";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import Stacker from "../middlewares/stacker";
|
||||||
|
import AccountService from "./services/account";
|
||||||
|
import LoginService from "./services/login";
|
||||||
|
import SecurityService from "./services/security";
|
||||||
|
import TFAService from "./services/twofactor";
|
||||||
|
|
||||||
|
export type SessionContext = Request;
|
||||||
|
|
||||||
|
const provider = new Server.ServiceProvider<SessionContext>();
|
||||||
|
provider.addService(new AccountService());
|
||||||
|
provider.addService(new SecurityService());
|
||||||
|
provider.addService(new TFAService());
|
||||||
|
provider.addService(new LoginService());
|
||||||
|
|
||||||
|
const JRPCEndpoint = Stacker(
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let jrpcreq = req.body as RequestObject;
|
||||||
|
let startTime = process.hrtime.bigint();
|
||||||
|
const session = provider.getSession((data: ResponseObject) => {
|
||||||
|
let time = process.hrtime.bigint() - startTime;
|
||||||
|
let state = data.error ? Format.red(`err(${data.error.message})`) : Format.green("OK");
|
||||||
|
|
||||||
|
Logging.getChild("JRPC").log(jrpcreq.method, state, "-", (Number(time / 10000n) / 100) + "ms");
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
}, req);
|
||||||
|
|
||||||
|
|
||||||
|
session.onMessage(jrpcreq);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default JRPCEndpoint;
|
52
Backend/src/api/jrpc/services/account.ts
Normal file
52
Backend/src/api/jrpc/services/account.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Profile, ContactInfo, Gender, Server, UserRegisterInfo } from "@hibas123/openauth-internalapi";
|
||||||
|
import type { SessionContext } from "../index";
|
||||||
|
import Mail from "../../../models/mail";
|
||||||
|
import User from "../../../models/user";
|
||||||
|
import { RequireLogin } from "../../../helper/login";
|
||||||
|
|
||||||
|
export default class AccountService extends Server.AccountService<SessionContext> {
|
||||||
|
Register(regcode: string, info: UserRegisterInfo, ctx: SessionContext): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async GetProfile(ctx: SessionContext): Promise<Profile> {
|
||||||
|
if (!ctx.user) throw new Error("Not logged in");
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: ctx.user.uid,
|
||||||
|
username: ctx.user.username,
|
||||||
|
name: ctx.user.name,
|
||||||
|
birthday: ctx.user.birthday.valueOf(),
|
||||||
|
gender: ctx.user.gender as number as Gender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async UpdateProfile(info: Profile, ctx: SessionContext): Promise<void> {
|
||||||
|
if (!ctx.user) throw new Error("Not logged in");
|
||||||
|
|
||||||
|
ctx.user.name = info.name;
|
||||||
|
ctx.user.birthday = new Date(info.birthday);
|
||||||
|
ctx.user.gender = info.gender as number;
|
||||||
|
|
||||||
|
await User.save(ctx.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async GetContactInfos(ctx: SessionContext): Promise<ContactInfo> {
|
||||||
|
if (!ctx.user) throw new Error("Not logged in");
|
||||||
|
|
||||||
|
let mails = await Promise.all(
|
||||||
|
ctx.user.mails.map((mail) => Mail.findById(mail))
|
||||||
|
);
|
||||||
|
|
||||||
|
let contact = {
|
||||||
|
mail: mails.filter((e) => !!e),
|
||||||
|
phone: ctx.user.phones,
|
||||||
|
};
|
||||||
|
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
}
|
265
Backend/src/api/jrpc/services/login.ts
Normal file
265
Backend/src/api/jrpc/services/login.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import { Server, LoginState, TFAOption, TFAType } from "@hibas123/openauth-internalapi";
|
||||||
|
import type { SessionContext } from "../index";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
import User, { IUser } from "../../../models/user";
|
||||||
|
import moment from "moment";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import TwoFactor, { ITwoFactor, IWebAuthn } from "../../../models/twofactor";
|
||||||
|
import speakeasy from "speakeasy";
|
||||||
|
import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
|
||||||
|
import config from "../../../config";
|
||||||
|
|
||||||
|
//FIXME: There are a lot of uneccessary database requests happening here. Since this is not a "hot" path, it should not matter to much, but it should be fixed nontheless.
|
||||||
|
|
||||||
|
export default class LoginService extends Server.LoginService<SessionContext> {
|
||||||
|
private async getUser(username: string): Promise<IUser> {
|
||||||
|
let user = await User.findOne({ username: username.toLowerCase() });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLoginState(ctx: SessionContext): Promise<LoginState> {
|
||||||
|
if (ctx.user && ctx.session.validated) {
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
} else if (ctx.session.login_state) {
|
||||||
|
//TODO: Check login_state expiration or so
|
||||||
|
if (ctx.session.login_state.username) {
|
||||||
|
let user = await this.getUser(ctx.session.login_state.username);
|
||||||
|
if (!ctx.session.login_state.password_correct) {
|
||||||
|
let passwordSalt = user.salt;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
username: ctx.session.login_state.username,
|
||||||
|
password: false,
|
||||||
|
passwordSalt: passwordSalt,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let tfa = await this.getTwoFactors(await this.getUser(ctx.session.login_state.username))
|
||||||
|
if (tfa.length <= 0) {
|
||||||
|
ctx.session.user_id = user._id.toString();
|
||||||
|
ctx.session.login_state = undefined;
|
||||||
|
Logging.warn("This should have been set somewhere else!");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
username: ctx.session.login_state.username,
|
||||||
|
password: true,
|
||||||
|
requireTwoFactor: tfa,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
username: undefined,
|
||||||
|
password: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTwoFactors(user: IUser): Promise<TFAOption[]> {
|
||||||
|
let twofactors = await TwoFactor.find({
|
||||||
|
user: user._id,
|
||||||
|
valid: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return twofactors.map<TFAOption>(tf => {
|
||||||
|
return {
|
||||||
|
id: tf._id.toString(),
|
||||||
|
name: tf.name,
|
||||||
|
tfatype: tf.type as number,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async enableSession(ctx: SessionContext) {
|
||||||
|
let user = await this.getUser(ctx.session.login_state.username);
|
||||||
|
ctx.user = user;
|
||||||
|
ctx.session.user_id = user._id.toString();
|
||||||
|
ctx.session.login_state = undefined;
|
||||||
|
ctx.session.validated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetState(ctx: SessionContext): Promise<LoginState> {
|
||||||
|
return this.getLoginState(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Start(username: string, ctx: SessionContext): Promise<LoginState> {
|
||||||
|
let user = await this.getUser(username);
|
||||||
|
|
||||||
|
ctx.session.login_state = {
|
||||||
|
username: username,
|
||||||
|
password_correct: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getLoginState(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async UsePassword(password_hash: string, date: number, ctx: SessionContext): Promise<LoginState> {
|
||||||
|
if (!ctx.session.login_state) {
|
||||||
|
throw new Error("No login state. Call Start() first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await this.getUser(ctx.session.login_state.username);
|
||||||
|
|
||||||
|
if (date <= 0) {
|
||||||
|
if (user.password !== password_hash) {
|
||||||
|
throw new Error("Password incorrect");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!moment(date).isBetween(
|
||||||
|
moment().subtract(1, "minute"),
|
||||||
|
moment().add(1, "minute")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("Date incorrect. Please check your devices time!");
|
||||||
|
} else {
|
||||||
|
let upw = crypto
|
||||||
|
.createHash("sha512")
|
||||||
|
.update(user.password + date.toString())
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
if (upw !== password_hash) {
|
||||||
|
throw new Error("Password incorrect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.session.login_state.password_correct = true;
|
||||||
|
|
||||||
|
let tfas = await this.getTwoFactors(user);
|
||||||
|
if (tfas.length <= 0) {
|
||||||
|
await this.enableSession(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getLoginState(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async getAndCheckTFA<T extends ITwoFactor>(id: string, shouldType: TFAType, ctx: SessionContext): Promise<T> {
|
||||||
|
if (!ctx.session.login_state) {
|
||||||
|
throw new Error("No login state. Call Start() first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await this.getUser(ctx.session.login_state.username);
|
||||||
|
|
||||||
|
let tfa = await TwoFactor.findById(id);
|
||||||
|
if (!tfa || tfa.user.toString() != user._id.toString()) {
|
||||||
|
throw new Error("Two factor not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tfa.type != shouldType as number) {
|
||||||
|
throw new Error("Two factor is not the correct type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tfa.valid) {
|
||||||
|
throw new Error("Two factor is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tfa.expires && moment().isAfter(moment(tfa.expires))) {
|
||||||
|
throw new Error("Two factor is expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tfa as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async UseTOTP(id: string, code: string, ctx: SessionContext): Promise<LoginState> {
|
||||||
|
let tfa = await this.getAndCheckTFA(id, TFAType.TOTP, ctx);
|
||||||
|
|
||||||
|
let valid = speakeasy.totp.verify({
|
||||||
|
secret: tfa.data,
|
||||||
|
encoding: "base32",
|
||||||
|
token: code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error("Code incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.enableSession(ctx);
|
||||||
|
|
||||||
|
return this.getLoginState(ctx);
|
||||||
|
}
|
||||||
|
async UseBackupCode(id: string, code: string, ctx: SessionContext): Promise<LoginState> {
|
||||||
|
let tfa = await this.getAndCheckTFA(id, TFAType.BACKUP_CODE, ctx);
|
||||||
|
|
||||||
|
if (tfa.data.indexOf(code) < 0) {
|
||||||
|
throw new Error("Code incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
tfa.data = tfa.data.filter(c => c != code);
|
||||||
|
|
||||||
|
await TwoFactor.save(tfa);
|
||||||
|
//TODO: handle the case where the last backup code is used
|
||||||
|
|
||||||
|
await this.enableSession(ctx);
|
||||||
|
|
||||||
|
return this.getLoginState(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetWebAuthnChallenge(id: string, ctx: SessionContext): Promise<string> {
|
||||||
|
let tfa = await this.getAndCheckTFA<IWebAuthn>(id, TFAType.WEBAUTHN, ctx);
|
||||||
|
|
||||||
|
const rpID = new URL(config.core.url).hostname;
|
||||||
|
|
||||||
|
let options = generateAuthenticationOptions({
|
||||||
|
timeout: 60000,
|
||||||
|
userVerification: "discouraged",
|
||||||
|
rpID,
|
||||||
|
allowCredentials: [{
|
||||||
|
id: tfa.data.device.credentialID.buffer,
|
||||||
|
type: "public-key",
|
||||||
|
transports: tfa.data.device.transports
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.session.login_state.webauthn_challenge = options.challenge;
|
||||||
|
|
||||||
|
Logging.debug("Challenge", options, tfa, tfa.data.device.credentialID);
|
||||||
|
|
||||||
|
return JSON.stringify(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async UseWebAuthn(id: string, response: string, ctx: SessionContext): Promise<LoginState> {
|
||||||
|
let tfa = await this.getAndCheckTFA<IWebAuthn>(id, TFAType.WEBAUTHN, ctx);
|
||||||
|
|
||||||
|
if (!ctx.session.login_state.webauthn_challenge) {
|
||||||
|
throw new Error("No webauthn challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpID = new URL(config.core.url).hostname;
|
||||||
|
|
||||||
|
let verification = await verifyAuthenticationResponse({
|
||||||
|
response: JSON.parse(response),
|
||||||
|
authenticator: {
|
||||||
|
counter: tfa.data.device.counter,
|
||||||
|
credentialID: tfa.data.device.credentialID.buffer,
|
||||||
|
credentialPublicKey: tfa.data.device.credentialPublicKey.buffer,
|
||||||
|
transports: tfa.data.device.transports
|
||||||
|
},
|
||||||
|
expectedChallenge: ctx.session.login_state.webauthn_challenge,
|
||||||
|
expectedOrigin: config.core.url,
|
||||||
|
expectedRPID: rpID,
|
||||||
|
requireUserVerification: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (verification.verified) {
|
||||||
|
tfa.data.device.counter = verification.authenticationInfo.newCounter;
|
||||||
|
await TwoFactor.save(tfa);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.enableSession(ctx);
|
||||||
|
|
||||||
|
return this.getLoginState(ctx);
|
||||||
|
}
|
||||||
|
}
|
35
Backend/src/api/jrpc/services/security.ts
Normal file
35
Backend/src/api/jrpc/services/security.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Server, Session } from "@hibas123/openauth-internalapi";
|
||||||
|
import type { SessionContext } from "../index";
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
import { RequireLogin } from "../../../helper/login";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import User from "../../../models/user";
|
||||||
|
|
||||||
|
export default class SecurityService extends Server.SecurityService<SessionContext> {
|
||||||
|
@RequireLogin()
|
||||||
|
async GetSessions(ctx: SessionContext): Promise<Session[]> {
|
||||||
|
return []
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
@RequireLogin()
|
||||||
|
async RevokeSession(id: string, ctx: SessionContext): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async ChangePassword(old_pw: string, new_pw: string, ctx: SessionContext): Promise<void> {
|
||||||
|
let old_pw_hash = crypto.createHash("sha512").update(ctx.user.salt + old_pw).digest("hex");
|
||||||
|
|
||||||
|
if (old_pw_hash != ctx.user.password) {
|
||||||
|
throw new Error("Wrong password");
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt = crypto.randomBytes(32).toString("base64");
|
||||||
|
let password_hash = crypto.createHash("sha512").update(salt + new_pw).digest("hex");
|
||||||
|
|
||||||
|
ctx.user.salt = salt;
|
||||||
|
ctx.user.password = password_hash;
|
||||||
|
|
||||||
|
await User.save(ctx.user);
|
||||||
|
}
|
||||||
|
}
|
194
Backend/src/api/jrpc/services/twofactor.ts
Normal file
194
Backend/src/api/jrpc/services/twofactor.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { TFANewTOTP, Server, TFAOption, UserRegisterInfo, TFAWebAuthRegister } from "@hibas123/openauth-internalapi";
|
||||||
|
import type { SessionContext } from "../index";
|
||||||
|
import TwoFactorModel, { ITOTP, IWebAuthn, TFATypes } from "../../../models/twofactor";
|
||||||
|
import moment = require("moment");
|
||||||
|
import * as speakeasy from "speakeasy";
|
||||||
|
import * as qrcode from "qrcode";
|
||||||
|
import config from "../../../config";
|
||||||
|
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
|
||||||
|
import type { RegistrationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||||
|
import Logging from "@hibas123/nodelogging";
|
||||||
|
import { Binary } from "mongodb";
|
||||||
|
import { RequireLogin } from "../../../helper/login";
|
||||||
|
|
||||||
|
|
||||||
|
export default class TFAService extends Server.TFAService<SessionContext> {
|
||||||
|
@RequireLogin()
|
||||||
|
AddBackupCodes(name: string, ctx: SessionContext): Promise<string[]> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
RemoveBackupCodes(id: string, ctx: SessionContext): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async GetOptions(ctx: SessionContext): Promise<TFAOption[]> {
|
||||||
|
let twofactor = await TwoFactorModel.find({ user: ctx.user._id, valid: true });
|
||||||
|
let expired = twofactor.filter((e) =>
|
||||||
|
e.expires ? moment().isAfter(moment(e.expires)) : false
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
expired.map((e) => {
|
||||||
|
e.valid = false;
|
||||||
|
return TwoFactorModel.save(e);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
twofactor = twofactor.filter((e) => e.valid);
|
||||||
|
let tfa = twofactor.map<TFAOption>((e) => {
|
||||||
|
return {
|
||||||
|
id: e._id.toString(),
|
||||||
|
name: e.name,
|
||||||
|
tfatype: e.type as number,
|
||||||
|
expires: e.expires?.valueOf()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return tfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async Delete(id: string, ctx: SessionContext): Promise<void> {
|
||||||
|
let twofactor = await TwoFactorModel.findById(id);
|
||||||
|
if (!twofactor || !twofactor.user.equals(ctx.user._id))
|
||||||
|
throw new Error("Invalid ID");
|
||||||
|
|
||||||
|
twofactor.valid = false;
|
||||||
|
await TwoFactorModel.save(twofactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async AddTOTP(name: string, ctx: SessionContext): Promise<TFANewTOTP> {
|
||||||
|
//Generating new
|
||||||
|
let secret = speakeasy.generateSecret({
|
||||||
|
name: config.core.name,
|
||||||
|
issuer: config.core.name,
|
||||||
|
otpauth_url: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let twofactor = TwoFactorModel.new(<ITOTP>{
|
||||||
|
name: name,
|
||||||
|
user: ctx.user._id,
|
||||||
|
type: TFATypes.TOTP,
|
||||||
|
valid: false,
|
||||||
|
data: secret.base32,
|
||||||
|
});
|
||||||
|
|
||||||
|
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
||||||
|
await TwoFactorModel.save(twofactor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: twofactor._id.toString(),
|
||||||
|
qr: dataurl,
|
||||||
|
secret: secret.base32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async VerifyTOTP(id: string, code: string, ctx: SessionContext): Promise<void> {
|
||||||
|
let twofactor = await TwoFactorModel.findById(id);
|
||||||
|
if (!twofactor || !twofactor.user.equals(ctx.user._id))
|
||||||
|
throw new Error("Invalid ID");
|
||||||
|
|
||||||
|
let verified = speakeasy.totp.verify({
|
||||||
|
secret: twofactor.data,
|
||||||
|
encoding: "base32",
|
||||||
|
token: code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!verified) throw new Error("Invalid code");
|
||||||
|
|
||||||
|
twofactor.valid = true;
|
||||||
|
twofactor.expires = undefined;
|
||||||
|
|
||||||
|
await TwoFactorModel.save(twofactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async AddWebauthn(name: string, ctx: SessionContext): Promise<TFAWebAuthRegister> {
|
||||||
|
// TODO: Get already registered options
|
||||||
|
|
||||||
|
const rpID = new URL(config.core.url).hostname;
|
||||||
|
const options = generateRegistrationOptions({
|
||||||
|
rpName: config.core.name,
|
||||||
|
rpID,
|
||||||
|
userID: ctx.user.uid,
|
||||||
|
userName: ctx.user.username,
|
||||||
|
attestationType: 'direct',
|
||||||
|
userDisplayName: ctx.user.name,
|
||||||
|
excludeCredentials: [],
|
||||||
|
authenticatorSelection: {
|
||||||
|
userVerification: "required",
|
||||||
|
requireResidentKey: false,
|
||||||
|
residentKey: "discouraged",
|
||||||
|
authenticatorAttachment: "cross-platform"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const twofactor = TwoFactorModel.new({
|
||||||
|
name,
|
||||||
|
type: TFATypes.WEBAUTHN,
|
||||||
|
user: ctx.user._id,
|
||||||
|
valid: false,
|
||||||
|
data: {
|
||||||
|
challenge: options.challenge
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await TwoFactorModel.save(twofactor);
|
||||||
|
|
||||||
|
Logging.debug(twofactor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: twofactor._id.toString(),
|
||||||
|
challenge: JSON.stringify(options)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequireLogin()
|
||||||
|
async VerifyWebAuthn(id: string, registration: string, ctx: SessionContext): Promise<void> {
|
||||||
|
let twofactor = await TwoFactorModel.findById(id) as IWebAuthn;
|
||||||
|
if (!twofactor || !twofactor.user.equals(ctx.user._id))
|
||||||
|
throw new Error("Invalid ID");
|
||||||
|
|
||||||
|
const rpID = new URL(config.core.url).hostname;
|
||||||
|
|
||||||
|
const response = JSON.parse(registration) as RegistrationResponseJSON;
|
||||||
|
|
||||||
|
let verification = await verifyRegistrationResponse({
|
||||||
|
response,
|
||||||
|
expectedChallenge: twofactor.data.challenge,
|
||||||
|
expectedOrigin: config.core.url,
|
||||||
|
expectedRPID: rpID,
|
||||||
|
requireUserVerification: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verification.verified) {
|
||||||
|
const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;
|
||||||
|
|
||||||
|
//TODO: Check if already registered!
|
||||||
|
// TwoFactorModel.find({
|
||||||
|
// data: {
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
twofactor.data = {
|
||||||
|
device: {
|
||||||
|
credentialPublicKey: new Binary(credentialPublicKey),
|
||||||
|
credentialID: new Binary(credentialID),
|
||||||
|
counter: verification.registrationInfo.counter,
|
||||||
|
transports: response.response.transports as any[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
twofactor.valid = true;
|
||||||
|
|
||||||
|
await TwoFactorModel.save(twofactor);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
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 User from "../../models/user";
|
|
||||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||||
|
import { requireLoginState } from "../../helper/login";
|
||||||
|
|
||||||
class Invalid extends Error { }
|
class Invalid extends Error { }
|
||||||
|
|
||||||
@ -31,49 +30,14 @@ export function GetUserMiddleware(
|
|||||||
throw new Invalid(req.__(message));
|
throw new Invalid(req.__(message));
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
let { login, special } = req.query as { [key: string]: string };
|
if (!requireLoginState(req, validated, special_required)) {
|
||||||
if (!login) {
|
invalid("Not logged in");
|
||||||
login = req.cookies.login;
|
|
||||||
special = req.cookies.special;
|
|
||||||
}
|
}
|
||||||
if (!login) invalid("No login token");
|
|
||||||
if (!special && special_required) invalid("No special token");
|
|
||||||
|
|
||||||
let token = await LoginToken.findOne({ token: login, valid: true });
|
|
||||||
if (!(await CheckToken(token, validated)))
|
|
||||||
invalid("Login token invalid");
|
|
||||||
|
|
||||||
let user = await User.findById(token.user);
|
|
||||||
if (!user) {
|
|
||||||
token.valid = false;
|
|
||||||
await LoginToken.save(token);
|
|
||||||
invalid("Login token invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
let special_token;
|
|
||||||
if (special) {
|
|
||||||
Logging.debug("Special found");
|
|
||||||
special_token = await LoginToken.findOne({
|
|
||||||
token: special,
|
|
||||||
special: true,
|
|
||||||
valid: true,
|
|
||||||
user: token.user,
|
|
||||||
});
|
|
||||||
if (!(await CheckToken(special_token, validated)))
|
|
||||||
invalid("Special token invalid");
|
|
||||||
req.special = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = user;
|
|
||||||
req.isAdmin = user.admin;
|
|
||||||
req.token = {
|
|
||||||
login: token,
|
|
||||||
special: special_token,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (next) next();
|
if (next) next();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Logging.getChild("UserMiddleware").warn(e);
|
||||||
if (e instanceof Invalid) {
|
if (e instanceof Invalid) {
|
||||||
if (req.method === "GET" && !json) {
|
if (req.method === "GET" && !json) {
|
||||||
res.status(HttpStatusCode.UNAUTHORIZED);
|
res.status(HttpStatusCode.UNAUTHORIZED);
|
||||||
|
@ -7,10 +7,10 @@ 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;
|
||||||
@ -51,7 +51,7 @@ import { ObjectID } from "mongodb";
|
|||||||
|
|
||||||
// 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) {
|
||||||
@ -128,7 +128,7 @@ const GetAuthRoute = (view = false) =>
|
|||||||
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;
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import Stacker from "../middlewares/stacker";
|
|
||||||
import { GetUserMiddleware } from "../middlewares/user";
|
|
||||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
|
||||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
|
||||||
|
|
||||||
export const GetAccount = Stacker(
|
|
||||||
GetUserMiddleware(true, true),
|
|
||||||
async (req: Request, res: Response) => {
|
|
||||||
let user = {
|
|
||||||
id: req.user.uid,
|
|
||||||
name: req.user.name,
|
|
||||||
username: req.user.username,
|
|
||||||
birthday: req.user.birthday,
|
|
||||||
gender: req.user.gender,
|
|
||||||
};
|
|
||||||
res.json({ user });
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import Stacker from "../middlewares/stacker";
|
|
||||||
import { GetUserMiddleware } from "../middlewares/user";
|
|
||||||
import Mail from "../../models/mail";
|
|
||||||
|
|
||||||
export const GetContactInfos = Stacker(
|
|
||||||
GetUserMiddleware(true, true),
|
|
||||||
async (req: Request, res: Response) => {
|
|
||||||
let mails = await Promise.all(
|
|
||||||
req.user.mails.map((mail) => Mail.findById(mail))
|
|
||||||
);
|
|
||||||
|
|
||||||
let contact = {
|
|
||||||
mails: mails.filter((e) => !!e),
|
|
||||||
phones: req.user.phones,
|
|
||||||
};
|
|
||||||
res.json({ contact });
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,10 +1,5 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { GetAccount } from "./account";
|
|
||||||
import { GetContactInfos } from "./contact";
|
|
||||||
import Login from "./login";
|
|
||||||
import Register from "./register";
|
import Register from "./register";
|
||||||
import { DeleteToken, GetToken } from "./token";
|
|
||||||
import TwoFactorRoute from "./twofactor";
|
|
||||||
import OAuthRoute from "./oauth";
|
import OAuthRoute from "./oauth";
|
||||||
|
|
||||||
const UserRoute: Router = Router();
|
const UserRoute: Router = Router();
|
||||||
@ -39,94 +34,6 @@ const UserRoute: Router = Router();
|
|||||||
*/
|
*/
|
||||||
UserRoute.post("/register", Register);
|
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);
|
UserRoute.use("/oauth", OAuthRoute);
|
||||||
|
|
||||||
export default UserRoute;
|
export default UserRoute;
|
||||||
|
@ -1,134 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import User, { IUser } from "../../models/user";
|
|
||||||
import { randomBytes } from "crypto";
|
|
||||||
import moment = require("moment");
|
|
||||||
import LoginToken from "../../models/login_token";
|
|
||||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
|
||||||
import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
|
|
||||||
import * as crypto from "crypto";
|
|
||||||
import Logging from "@hibas123/nodelogging";
|
|
||||||
|
|
||||||
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
|
||||||
let type = req.query.type as string;
|
|
||||||
if (type === "username") {
|
|
||||||
let { username, uid } = req.query as { [key: string]: string };
|
|
||||||
let user = await User.findOne(
|
|
||||||
username ? { username: username.toLowerCase() } : { uid: uid }
|
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
res.json({ error: req.__("User not found") });
|
|
||||||
} else {
|
|
||||||
res.json({ salt: user.salt, uid: user.uid });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (type === "password") {
|
|
||||||
const sendToken = async (user: IUser, tfa?: any[]) => {
|
|
||||||
let ip =
|
|
||||||
req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
|
||||||
let client = {
|
|
||||||
ip: Array.isArray(ip) ? ip[0] : ip,
|
|
||||||
browser: req.headers["user-agent"],
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_str = randomBytes(16).toString("hex");
|
|
||||||
let tfa_exp = moment().add(5, "minutes").toDate();
|
|
||||||
let token_exp = moment().add(6, "months").toDate();
|
|
||||||
let token = LoginToken.new({
|
|
||||||
token: token_str,
|
|
||||||
valid: true,
|
|
||||||
validTill: tfa ? tfa_exp : token_exp,
|
|
||||||
user: user._id,
|
|
||||||
validated: tfa ? false : true,
|
|
||||||
...client,
|
|
||||||
});
|
|
||||||
await LoginToken.save(token);
|
|
||||||
|
|
||||||
let special_str = randomBytes(24).toString("hex");
|
|
||||||
let special_exp = moment().add(30, "minutes").toDate();
|
|
||||||
let special = LoginToken.new({
|
|
||||||
token: special_str,
|
|
||||||
valid: true,
|
|
||||||
validTill: tfa ? tfa_exp : special_exp,
|
|
||||||
special: true,
|
|
||||||
user: user._id,
|
|
||||||
validated: tfa ? false : true,
|
|
||||||
...client,
|
|
||||||
});
|
|
||||||
await LoginToken.save(special);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
login: { token: token_str, expires: token.validTill.toUTCString() },
|
|
||||||
special: {
|
|
||||||
token: special_str,
|
|
||||||
expires: special.validTill.toUTCString(),
|
|
||||||
},
|
|
||||||
tfa,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let { username, password, uid, date } = req.body;
|
|
||||||
|
|
||||||
let user = await User.findOne(
|
|
||||||
username ? { username: username.toLowerCase() } : { uid: uid }
|
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
res.json({ error: req.__("User not found") });
|
|
||||||
} else {
|
|
||||||
let upw = user.password;
|
|
||||||
if (date) {
|
|
||||||
if (
|
|
||||||
!moment(date).isBetween(
|
|
||||||
moment().subtract(1, "minute"),
|
|
||||||
moment().add(1, "minute")
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
res.json({
|
|
||||||
error: req.__(
|
|
||||||
"Invalid timestamp. Please check your devices time!"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
upw = crypto
|
|
||||||
.createHash("sha512")
|
|
||||||
.update(upw + date.toString())
|
|
||||||
.digest("hex");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (upw !== password) {
|
|
||||||
res.json({ error: req.__("Password or username wrong") });
|
|
||||||
} else {
|
|
||||||
let twofactor = await TwoFactor.find({
|
|
||||||
user: user._id,
|
|
||||||
valid: true,
|
|
||||||
});
|
|
||||||
let expired = twofactor.filter((e) =>
|
|
||||||
e.expires ? moment().isAfter(moment(e.expires)) : false
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
expired.map((e) => {
|
|
||||||
e.valid = false;
|
|
||||||
return TwoFactor.save(e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
twofactor = twofactor.filter((e) => e.valid);
|
|
||||||
if (twofactor && twofactor.length > 0) {
|
|
||||||
let tfa = twofactor.map((e) => {
|
|
||||||
return {
|
|
||||||
id: e._id,
|
|
||||||
name: e.name || TFANames.get(e.type),
|
|
||||||
type: e.type,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await sendToken(user, tfa);
|
|
||||||
} else {
|
|
||||||
await sendToken(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.json({ error: req.__("Invalid type!") });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Login;
|
|
@ -1,45 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import Stacker from "../middlewares/stacker";
|
|
||||||
import { GetUserMiddleware } from "../middlewares/user";
|
|
||||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
|
||||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
|
||||||
|
|
||||||
export const GetToken = Stacker(
|
|
||||||
GetUserMiddleware(true, true),
|
|
||||||
async (req: Request, res: Response) => {
|
|
||||||
let raw_token = await LoginToken.find({
|
|
||||||
user: req.user._id,
|
|
||||||
valid: true,
|
|
||||||
});
|
|
||||||
let token = await Promise.all(
|
|
||||||
raw_token
|
|
||||||
.map(async (token) => {
|
|
||||||
await CheckToken(token);
|
|
||||||
return {
|
|
||||||
id: token._id,
|
|
||||||
special: token.special,
|
|
||||||
ip: token.ip,
|
|
||||||
browser: token.browser,
|
|
||||||
isthis: token._id.equals(
|
|
||||||
token.special ? req.token.special._id : req.token.login._id
|
|
||||||
),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((t) => t !== undefined)
|
|
||||||
);
|
|
||||||
res.json({ token });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DeleteToken = Stacker(
|
|
||||||
GetUserMiddleware(true, true),
|
|
||||||
async (req: Request, res: Response) => {
|
|
||||||
let { id } = req.params;
|
|
||||||
let token = await LoginToken.findById(id);
|
|
||||||
if (!token || !token.user.equals(req.user._id))
|
|
||||||
throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
|
|
||||||
token.valid = false;
|
|
||||||
await LoginToken.save(token);
|
|
||||||
res.json({ success: true });
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,100 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import Stacker from "../../../middlewares/stacker";
|
|
||||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
|
||||||
import TwoFactor, {
|
|
||||||
TFATypes as TwoFATypes,
|
|
||||||
IBackupCode,
|
|
||||||
} from "../../../../models/twofactor";
|
|
||||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
|
||||||
import moment = require("moment");
|
|
||||||
import { upgradeToken } from "../helper";
|
|
||||||
import * as crypto from "crypto";
|
|
||||||
import Logging from "@hibas123/nodelogging";
|
|
||||||
|
|
||||||
const BackupCodeRoute = Router();
|
|
||||||
|
|
||||||
// TODO: Further checks if this is good enough randomness
|
|
||||||
function generateCode(length: number) {
|
|
||||||
let bytes = crypto.randomBytes(length);
|
|
||||||
let nrs = "";
|
|
||||||
bytes.forEach((b, idx) => {
|
|
||||||
let nr = Math.floor((b / 255) * 9.9999);
|
|
||||||
if (nr > 9) nr = 9;
|
|
||||||
nrs += String(nr);
|
|
||||||
});
|
|
||||||
return nrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
BackupCodeRoute.post(
|
|
||||||
"/",
|
|
||||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
|
||||||
//Generating new
|
|
||||||
let codes = Array(10).map(() => generateCode(8));
|
|
||||||
console.log(codes);
|
|
||||||
let twofactor = TwoFactor.new(<IBackupCode>{
|
|
||||||
user: req.user._id,
|
|
||||||
type: TwoFATypes.OTC,
|
|
||||||
valid: true,
|
|
||||||
data: codes,
|
|
||||||
name: "",
|
|
||||||
});
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
res.json({
|
|
||||||
codes,
|
|
||||||
id: twofactor._id,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
BackupCodeRoute.put(
|
|
||||||
"/",
|
|
||||||
Stacker(
|
|
||||||
GetUserMiddleware(true, false, undefined, false),
|
|
||||||
async (req, res) => {
|
|
||||||
let { login, special } = req.token;
|
|
||||||
let { id, code }: { id: string; code: string } = req.body;
|
|
||||||
|
|
||||||
let twofactor: IBackupCode = await TwoFactor.findById(id);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!twofactor ||
|
|
||||||
!twofactor.valid ||
|
|
||||||
!twofactor.user.equals(req.user._id) ||
|
|
||||||
twofactor.type !== TwoFATypes.OTC
|
|
||||||
) {
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
|
||||||
twofactor.valid = false;
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
code = code.replace(/\s/g, "");
|
|
||||||
let valid = twofactor.data.find((c) => c === code);
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
twofactor.data = twofactor.data.filter((c) => c !== code);
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
let [login_exp, special_exp] = await Promise.all([
|
|
||||||
upgradeToken(login),
|
|
||||||
upgradeToken(special),
|
|
||||||
]);
|
|
||||||
res.json({ success: true, login_exp, special_exp });
|
|
||||||
} else {
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid or already used code!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default BackupCodeRoute;
|
|
@ -1,16 +0,0 @@
|
|||||||
import LoginToken, { ILoginToken } from "../../../models/login_token";
|
|
||||||
import moment = require("moment");
|
|
||||||
|
|
||||||
export async function upgradeToken(token: ILoginToken) {
|
|
||||||
token.data = undefined;
|
|
||||||
token.valid = true;
|
|
||||||
token.validated = true;
|
|
||||||
//TODO durations from config
|
|
||||||
let expires = (token.special
|
|
||||||
? moment().add(30, "minute")
|
|
||||||
: moment().add(6, "months")
|
|
||||||
).toDate();
|
|
||||||
token.validTill = expires;
|
|
||||||
await LoginToken.save(token);
|
|
||||||
return expires;
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import YubiKeyRoute from "./yubikey";
|
|
||||||
import { GetUserMiddleware } from "../../middlewares/user";
|
|
||||||
import Stacker from "../../middlewares/stacker";
|
|
||||||
import TwoFactor from "../../../models/twofactor";
|
|
||||||
import * as moment from "moment";
|
|
||||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
|
||||||
import OTCRoute from "./otc";
|
|
||||||
import BackupCodeRoute from "./backup";
|
|
||||||
|
|
||||||
const TwoFactorRouter = Router();
|
|
||||||
|
|
||||||
TwoFactorRouter.get(
|
|
||||||
"/",
|
|
||||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
|
||||||
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true });
|
|
||||||
let expired = twofactor.filter((e) =>
|
|
||||||
e.expires ? moment().isAfter(moment(e.expires)) : false
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
expired.map((e) => {
|
|
||||||
e.valid = false;
|
|
||||||
return TwoFactor.save(e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
twofactor = twofactor.filter((e) => e.valid);
|
|
||||||
let tfa = twofactor.map((e) => {
|
|
||||||
return {
|
|
||||||
id: e._id,
|
|
||||||
name: e.name,
|
|
||||||
type: e.type,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
res.json({ methods: tfa });
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
TwoFactorRouter.delete(
|
|
||||||
"/:id",
|
|
||||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
|
||||||
let { id } = req.params;
|
|
||||||
let tfa = await TwoFactor.findById(id);
|
|
||||||
if (!tfa || !tfa.user.equals(req.user._id)) {
|
|
||||||
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
tfa.valid = false;
|
|
||||||
await TwoFactor.save(tfa);
|
|
||||||
res.json({ success: true });
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
|
|
||||||
TwoFactorRouter.use("/otc", OTCRoute);
|
|
||||||
TwoFactorRouter.use("/backup", BackupCodeRoute);
|
|
||||||
|
|
||||||
export default TwoFactorRouter;
|
|
@ -1,135 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import Stacker from "../../../middlewares/stacker";
|
|
||||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
|
||||||
import TwoFactor, {
|
|
||||||
TFATypes as TwoFATypes,
|
|
||||||
IOTC,
|
|
||||||
} from "../../../../models/twofactor";
|
|
||||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
|
||||||
import moment = require("moment");
|
|
||||||
import { upgradeToken } from "../helper";
|
|
||||||
import Logging from "@hibas123/nodelogging";
|
|
||||||
|
|
||||||
import * as speakeasy from "speakeasy";
|
|
||||||
import * as qrcode from "qrcode";
|
|
||||||
import config from "../../../../config";
|
|
||||||
|
|
||||||
const OTCRoute = Router();
|
|
||||||
|
|
||||||
OTCRoute.post(
|
|
||||||
"/",
|
|
||||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
|
||||||
const { type } = req.query;
|
|
||||||
if (type === "create") {
|
|
||||||
//Generating new
|
|
||||||
let secret = speakeasy.generateSecret({
|
|
||||||
name: config.core.name,
|
|
||||||
issuer: config.core.name,
|
|
||||||
});
|
|
||||||
let twofactor = TwoFactor.new(<IOTC>{
|
|
||||||
user: req.user._id,
|
|
||||||
type: TwoFATypes.OTC,
|
|
||||||
valid: false,
|
|
||||||
data: secret.base32,
|
|
||||||
});
|
|
||||||
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
res.json({
|
|
||||||
image: dataurl,
|
|
||||||
id: twofactor._id,
|
|
||||||
});
|
|
||||||
} else if (type === "validate") {
|
|
||||||
// Checking code and marking as valid
|
|
||||||
const { code, id } = req.body;
|
|
||||||
Logging.debug(req.body, id);
|
|
||||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
|
||||||
const err = () => {
|
|
||||||
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
|
||||||
};
|
|
||||||
if (
|
|
||||||
!twofactor ||
|
|
||||||
!twofactor.user.equals(req.user._id) ||
|
|
||||||
twofactor.type !== TwoFATypes.OTC ||
|
|
||||||
!twofactor.data ||
|
|
||||||
twofactor.valid
|
|
||||||
) {
|
|
||||||
Logging.debug("Not found or wrong user", twofactor);
|
|
||||||
err();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
|
||||||
await TwoFactor.delete(twofactor);
|
|
||||||
Logging.debug("Expired!", twofactor);
|
|
||||||
err();
|
|
||||||
}
|
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
|
||||||
secret: twofactor.data,
|
|
||||||
encoding: "base32",
|
|
||||||
token: code,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
twofactor.expires = undefined;
|
|
||||||
twofactor.valid = true;
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
OTCRoute.put(
|
|
||||||
"/",
|
|
||||||
Stacker(
|
|
||||||
GetUserMiddleware(true, false, undefined, false),
|
|
||||||
async (req, res) => {
|
|
||||||
let { login, special } = req.token;
|
|
||||||
let { id, code } = req.body;
|
|
||||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!twofactor ||
|
|
||||||
!twofactor.valid ||
|
|
||||||
!twofactor.user.equals(req.user._id) ||
|
|
||||||
twofactor.type !== TwoFATypes.OTC
|
|
||||||
) {
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
|
||||||
twofactor.valid = false;
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
|
||||||
secret: twofactor.data,
|
|
||||||
encoding: "base32",
|
|
||||||
token: code,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
let [login_exp, special_exp] = await Promise.all([
|
|
||||||
upgradeToken(login),
|
|
||||||
upgradeToken(special),
|
|
||||||
]);
|
|
||||||
res.json({ success: true, login_exp, special_exp });
|
|
||||||
} else {
|
|
||||||
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default OTCRoute;
|
|
@ -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.U2F,
|
|
||||||
valid: false,
|
|
||||||
data: {
|
|
||||||
registration: registrationRequest,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
res.json({
|
|
||||||
request: registrationRequest,
|
|
||||||
id: twofactor._id,
|
|
||||||
appid: config.core.url,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { response, id } = req.body;
|
|
||||||
Logging.debug(req.body, id);
|
|
||||||
let twofactor: IYubiKey = await TwoFactor.findById(id);
|
|
||||||
const err = () => {
|
|
||||||
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
|
||||||
};
|
|
||||||
if (
|
|
||||||
!twofactor ||
|
|
||||||
!twofactor.user.equals(req.user._id) ||
|
|
||||||
twofactor.type !== TwoFATypes.U2F ||
|
|
||||||
!twofactor.data.registration ||
|
|
||||||
twofactor.valid
|
|
||||||
) {
|
|
||||||
Logging.debug("Not found or wrong user", twofactor);
|
|
||||||
err();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
|
||||||
await TwoFactor.delete(twofactor);
|
|
||||||
Logging.debug("Expired!", twofactor);
|
|
||||||
err();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = u2f.checkRegistration(
|
|
||||||
twofactor.data.registration,
|
|
||||||
response
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.successful) {
|
|
||||||
twofactor.data = {
|
|
||||||
keyHandle: result.keyHandle,
|
|
||||||
publicKey: result.publicKey,
|
|
||||||
};
|
|
||||||
twofactor.expires = undefined;
|
|
||||||
twofactor.valid = true;
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
throw new RequestError(
|
|
||||||
result.errorMessage,
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
U2FRoute.get(
|
|
||||||
"/",
|
|
||||||
Stacker(
|
|
||||||
GetUserMiddleware(true, false, undefined, false),
|
|
||||||
async (req, res) => {
|
|
||||||
let { login, special } = req.token;
|
|
||||||
let twofactor: IYubiKey = await TwoFactor.findOne({
|
|
||||||
user: req.user._id,
|
|
||||||
type: TwoFATypes.U2F,
|
|
||||||
valid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!twofactor) {
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twofactor.expires) {
|
|
||||||
if (moment().isAfter(twofactor.expires)) {
|
|
||||||
twofactor.valid = false;
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = u2f.request(config.core.url, twofactor.data.keyHandle);
|
|
||||||
login.data = {
|
|
||||||
type: "ykr",
|
|
||||||
request,
|
|
||||||
};
|
|
||||||
let r;
|
|
||||||
if (special) {
|
|
||||||
special.data = login.data;
|
|
||||||
r = LoginToken.save(special);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([r, LoginToken.save(login)]);
|
|
||||||
res.json({ request });
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
U2FRoute.put(
|
|
||||||
"/",
|
|
||||||
Stacker(
|
|
||||||
GetUserMiddleware(true, false, undefined, false),
|
|
||||||
async (req, res) => {
|
|
||||||
let { login, special } = req.token;
|
|
||||||
let twofactor: IYubiKey = await TwoFactor.findOne({
|
|
||||||
user: req.user._id,
|
|
||||||
type: TwoFATypes.U2F,
|
|
||||||
valid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let { response } = req.body;
|
|
||||||
if (
|
|
||||||
!twofactor ||
|
|
||||||
!login.data ||
|
|
||||||
login.data.type !== "ykr" ||
|
|
||||||
(special && (!special.data || special.data.type !== "ykr"))
|
|
||||||
) {
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
|
||||||
twofactor.valid = false;
|
|
||||||
await TwoFactor.save(twofactor);
|
|
||||||
throw new RequestError(
|
|
||||||
"Invalid Method!",
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let login_exp;
|
|
||||||
let special_exp;
|
|
||||||
let result = u2f.checkSignature(
|
|
||||||
login.data.request,
|
|
||||||
response,
|
|
||||||
twofactor.data.publicKey
|
|
||||||
);
|
|
||||||
if (result.successful) {
|
|
||||||
if (special) {
|
|
||||||
let result = u2f.checkSignature(
|
|
||||||
special.data.request,
|
|
||||||
response,
|
|
||||||
twofactor.data.publicKey
|
|
||||||
);
|
|
||||||
if (result.successful) {
|
|
||||||
special_exp = await upgradeToken(special);
|
|
||||||
} else {
|
|
||||||
throw new RequestError(
|
|
||||||
result.errorMessage,
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
login_exp = await upgradeToken(login);
|
|
||||||
} else {
|
|
||||||
throw new RequestError(
|
|
||||||
result.errorMessage,
|
|
||||||
HttpStatusCode.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.json({ success: true, login_exp, special_exp });
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default U2FRoute;
|
|
@ -21,6 +21,7 @@ export interface CoreConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
|
secret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@ -41,6 +42,11 @@ const config = (parse(
|
|||||||
default: "Open Auth",
|
default: "Open Auth",
|
||||||
},
|
},
|
||||||
url: String,
|
url: String,
|
||||||
|
secret: {
|
||||||
|
type: String,
|
||||||
|
optional: false,
|
||||||
|
description: "Cookie secret"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
database: {
|
database: {
|
||||||
database: {
|
database: {
|
||||||
|
@ -8,6 +8,6 @@ if (Config.database) {
|
|||||||
}
|
}
|
||||||
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;
|
||||||
|
15
Backend/src/express.d.ts
vendored
15
Backend/src/express.d.ts
vendored
@ -1,6 +1,5 @@
|
|||||||
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 {
|
||||||
@ -8,9 +7,17 @@ declare module "express" {
|
|||||||
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
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;
|
||||||
@ -39,7 +39,7 @@ export function getIDToken(user: IUser, client_id: string, nonce: string) {
|
|||||||
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(
|
||||||
|
29
Backend/src/helper/login.ts
Normal file
29
Backend/src/helper/login.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { SessionContext } from "../api/jrpc";
|
||||||
|
|
||||||
|
export function requireLoginState(ctx: SessionContext, validated: boolean = true, special: boolean = false): boolean {
|
||||||
|
if (!ctx.user) return false;
|
||||||
|
if (validated && !ctx.session.validated) return false;
|
||||||
|
|
||||||
|
if (special) {
|
||||||
|
//TODO: Implement something...
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequireLogin(validated = true, special = false) {
|
||||||
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
let original = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = function (...args: any[]) {
|
||||||
|
let ctx = args[args.length - 1] as SessionContext;
|
||||||
|
if (!ctx) throw new Error("Invalid request");
|
||||||
|
|
||||||
|
if (!requireLoginState(ctx, validated, special)) {
|
||||||
|
throw new Error("Not logged in");
|
||||||
|
}
|
||||||
|
|
||||||
|
return original.apply(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -49,7 +49,7 @@ if (fs.existsSync("./keys")) {
|
|||||||
} 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");
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
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;
|
||||||
@ -22,7 +22,7 @@ const Client = DB.addModel<IClient>({
|
|||||||
{
|
{
|
||||||
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 },
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,9 +17,9 @@ const ClientCode = DB.addModel<IClientCode>({
|
|||||||
{
|
{
|
||||||
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 },
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
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>({
|
||||||
@ -14,9 +14,9 @@ const Grant = DB.addModel<IGrant>({
|
|||||||
{
|
{
|
||||||
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 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
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;
|
||||||
@ -22,7 +22,7 @@ const LoginToken = DB.addModel<ILoginToken>({
|
|||||||
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 },
|
||||||
},
|
},
|
||||||
@ -34,7 +34,7 @@ const LoginToken = DB.addModel<ILoginToken>({
|
|||||||
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 },
|
||||||
@ -47,7 +47,7 @@ const LoginToken = DB.addModel<ILoginToken>({
|
|||||||
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 },
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const Permission = DB.addModel<IPermission>({
|
|||||||
schema: {
|
schema: {
|
||||||
name: { type: String },
|
name: { type: String },
|
||||||
description: { type: String },
|
description: { type: String },
|
||||||
client: { type: ObjectID },
|
client: { type: ObjectId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -27,7 +27,7 @@ const Permission = DB.addModel<IPermission>({
|
|||||||
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" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@ -19,8 +19,8 @@ const RefreshToken = DB.addModel<IRefreshToken>({
|
|||||||
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 },
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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 {
|
||||||
|
@ -1,38 +1,40 @@
|
|||||||
|
import { TFAType } from "@hibas123/openauth-internalapi";
|
||||||
import DB from "../database";
|
import DB from "../database";
|
||||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||||
import { ObjectID } from "bson";
|
import { ObjectId } from "bson";
|
||||||
|
import { Binary } from "mongodb";
|
||||||
|
|
||||||
export enum TFATypes {
|
export { TFAType as TFATypes };
|
||||||
OTC,
|
|
||||||
BACKUP_CODE,
|
|
||||||
U2F,
|
|
||||||
APP_ALLOW,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TFANames = new Map<TFATypes, string>();
|
|
||||||
TFANames.set(TFATypes.OTC, "Authenticator");
|
export const TFANames = new Map<TFAType, string>();
|
||||||
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
|
TFANames.set(TFAType.TOTP, "Authenticator");
|
||||||
TFANames.set(TFATypes.U2F, "Security Key (U2F)");
|
TFANames.set(TFAType.BACKUP_CODE, "Backup Codes");
|
||||||
TFANames.set(TFATypes.APP_ALLOW, "App Push");
|
TFANames.set(TFAType.WEBAUTHN, "Security Key (WebAuthn)");
|
||||||
|
TFANames.set(TFAType.APP_ALLOW, "App Push");
|
||||||
|
|
||||||
export interface ITwoFactor extends ModelDataBase {
|
export interface ITwoFactor extends ModelDataBase {
|
||||||
user: ObjectID;
|
user: ObjectId;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
expires?: Date;
|
expires?: Date;
|
||||||
name?: string;
|
name?: string;
|
||||||
type: TFATypes;
|
type: TFAType;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOTC extends ITwoFactor {
|
export interface ITOTP extends ITwoFactor {
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IYubiKey extends ITwoFactor {
|
export interface IWebAuthn extends ITwoFactor {
|
||||||
data: {
|
data: {
|
||||||
registration?: any;
|
challenge?: any;
|
||||||
publicKey: string;
|
device?: {
|
||||||
keyHandle: string;
|
credentialID: Binary;
|
||||||
|
credentialPublicKey: Binary;
|
||||||
|
counter: number;
|
||||||
|
transports: AuthenticatorTransport[]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ const TwoFactor = DB.addModel<ITwoFactor>({
|
|||||||
{
|
{
|
||||||
migration: (e) => { },
|
migration: (e) => { },
|
||||||
schema: {
|
schema: {
|
||||||
user: { type: ObjectID },
|
user: { type: ObjectId },
|
||||||
valid: { type: Boolean },
|
valid: { type: Boolean },
|
||||||
expires: { type: Date, optional: true },
|
expires: { type: Date, optional: true },
|
||||||
name: { type: String, optional: true },
|
name: { type: String, optional: true },
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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";
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export interface IUser extends ModelDataBase {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,18 @@ 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) {
|
||||||
@ -70,7 +69,7 @@ export default async function TestData() {
|
|||||||
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,
|
||||||
@ -94,8 +93,10 @@ export default async function TestData() {
|
|||||||
|
|
||||||
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",
|
||||||
type: 0,
|
type: 0,
|
||||||
valid: true,
|
valid: true,
|
||||||
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
||||||
@ -104,6 +105,28 @@ export default async function TestData() {
|
|||||||
await TwoFactor.save(t);
|
await TwoFactor.save(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let tw = await TwoFactor.findOne({ user: u._id, type: 2 });
|
||||||
|
// if (!tw) {
|
||||||
|
// Logging.log("Adding test WebAuthn")
|
||||||
|
// tw = TwoFactor.new({
|
||||||
|
// user: u._id,
|
||||||
|
// name: "WebAuthn",
|
||||||
|
// type: 2,
|
||||||
|
// valid: true,
|
||||||
|
// data: {
|
||||||
|
// device: {
|
||||||
|
// credentialPublicKey: Buffer.from("pQECAyYgASFYINiHCRopJIn1GoTXq7SpDTJR1nzocqOWhjvpYaKLzzhSIlggvuHhjABe8NxbOIGA11vrd5deUT5R30anpE7W7xzPcsk=", "base64"),
|
||||||
|
// credentialID: Buffer.from("i/BJiffx0bxjQ9Ptyvc9ORELXALxrvD6pad1Xc/2nDI=", "base64"),
|
||||||
|
// counter: 1,
|
||||||
|
// transports: [
|
||||||
|
// "usb"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// await TwoFactor.save(tw);
|
||||||
|
// }
|
||||||
|
|
||||||
let login_token = await LoginToken.findOne({ token: "test01" });
|
let login_token = await LoginToken.findOne({ token: "test01" });
|
||||||
if (login_token) await LoginToken.delete(login_token);
|
if (login_token) await LoginToken.delete(login_token);
|
||||||
|
|
||||||
@ -142,5 +165,6 @@ export default async function TestData() {
|
|||||||
// Logging.debug("OTC Code is:", code);
|
// Logging.debug("OTC Code is:", code);
|
||||||
// }, 1000)
|
// }, 1000)
|
||||||
|
|
||||||
console.log("Finished adding test data")
|
Logging.log("Finished adding test data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
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";
|
||||||
@ -49,7 +49,13 @@ ViewRouter.use(
|
|||||||
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(
|
||||||
|
"/static",
|
||||||
|
addCache,
|
||||||
|
ServeStatic(path.join(viewsv2_location, "../static"), { cacheControl: false, })
|
||||||
);
|
);
|
||||||
|
|
||||||
ViewRouter.get("/code", (req, res) => {
|
ViewRouter.get("/code", (req, res) => {
|
@ -1,17 +1,24 @@
|
|||||||
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 bodyparser from "body-parser";
|
||||||
import * as cookieparser from "cookie-parser";
|
import cookieparser from "cookie-parser";
|
||||||
|
import session from "express-session";
|
||||||
|
import MongoStore from "connect-mongo";
|
||||||
|
|
||||||
import * as i18n from "i18n";
|
import i18n from "i18n";
|
||||||
import * as compression from "compression";
|
import compression from "compression";
|
||||||
import ApiRouter from "./api";
|
import ApiRouter from "./api";
|
||||||
import ViewRouter from "./views/views";
|
import ViewRouter from "./views";
|
||||||
import RequestError, { HttpStatusCode } from "./helper/request_error";
|
import RequestError, { HttpStatusCode } from "./helper/request_error";
|
||||||
|
import DB from "./database";
|
||||||
|
import promiseMiddleware from "./helper/promiseMiddleware";
|
||||||
|
import User from "./models/user";
|
||||||
|
import LoginToken, { CheckToken } from "./models/login_token";
|
||||||
|
|
||||||
export default class Web {
|
export default class Web {
|
||||||
server: Express;
|
server: Express;
|
||||||
@ -21,6 +28,7 @@ export default class Web {
|
|||||||
this.server = express();
|
this.server = express();
|
||||||
this.port = Number(config.port);
|
this.port = Number(config.port);
|
||||||
this.registerMiddleware();
|
this.registerMiddleware();
|
||||||
|
this.registerUserSession();
|
||||||
this.registerEndpoints();
|
this.registerEndpoints();
|
||||||
this.registerErrorHandler();
|
this.registerErrorHandler();
|
||||||
}
|
}
|
||||||
@ -32,6 +40,23 @@ export default class Web {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerMiddleware() {
|
private registerMiddleware() {
|
||||||
|
this.server.use(session({
|
||||||
|
secret: config.core.secret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: MongoStore.create({
|
||||||
|
client: DB.getClient(),
|
||||||
|
dbName: DB.db.databaseName,
|
||||||
|
collectionName: "sessions",
|
||||||
|
autoRemove: "native",
|
||||||
|
touchAfter: 60 * 60 * 24,
|
||||||
|
}),
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 30 * 6,
|
||||||
|
secure: !config.core.dev,
|
||||||
|
sameSite: "strict",
|
||||||
|
}
|
||||||
|
}))
|
||||||
this.server.use(cookieparser());
|
this.server.use(cookieparser());
|
||||||
this.server.use(
|
this.server.use(
|
||||||
bodyparser.json(),
|
bodyparser.json(),
|
||||||
@ -49,22 +74,23 @@ export default class Web {
|
|||||||
finished = true;
|
finished = true;
|
||||||
let td = process.hrtime(start);
|
let td = process.hrtime(start);
|
||||||
let time = !to ? (td[0] * 1e3 + td[1] / 1e6).toFixed(2) : "--.--";
|
let time = !to ? (td[0] * 1e3 + td[1] / 1e6).toFixed(2) : "--.--";
|
||||||
let resColor = "";
|
let resFormat: (arg: any) => any = (arg) => arg;
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300)
|
if (res.statusCode >= 200 && res.statusCode < 300)
|
||||||
resColor = "\x1b[32m";
|
resFormat = Format.green;
|
||||||
//Green
|
//Green
|
||||||
else if (res.statusCode === 304 || res.statusCode === 302)
|
else if (res.statusCode === 304 || res.statusCode === 302)
|
||||||
resColor = "\x1b[33m";
|
resFormat = Format.yellow; //"\x1b[33m";
|
||||||
else if (res.statusCode >= 400 && res.statusCode < 500)
|
else if (res.statusCode >= 400 && res.statusCode < 500)
|
||||||
resColor = "\x1b[36m";
|
resFormat = Format.red; // "\x1b[36m";
|
||||||
//Cyan
|
//Cyan
|
||||||
else if (res.statusCode >= 500 && res.statusCode < 600)
|
else if (res.statusCode >= 500 && res.statusCode < 600)
|
||||||
resColor = "\x1b[31m"; //Red
|
resFormat = Format.cyan //"\x1b[31m"; //Red
|
||||||
|
|
||||||
let m = req.method;
|
let m = req.method;
|
||||||
while (m.length < 4) m += " ";
|
while (m.length < 4) m += " ";
|
||||||
Logging.log(
|
Logging.getChild("HTTP").log(
|
||||||
`${m} ${req.originalUrl} ${(req as any).language || ""
|
`${m} ${req.originalUrl} ${(req as any).language || ""
|
||||||
} ${resColor}${res.statusCode}\x1b[0m - ${time}ms`
|
}`, resFormat(res.statusCode), `- ${time}ms`
|
||||||
);
|
);
|
||||||
res.removeListener("finish", listener);
|
res.removeListener("finish", listener);
|
||||||
};
|
};
|
||||||
@ -119,4 +145,31 @@ export default class Web {
|
|||||||
} else res.status(error.status || 500).send(error.message);
|
} else res.status(error.status || 500).send(error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private registerUserSession() {
|
||||||
|
this.server.use(promiseMiddleware(async (req, res, next) => {
|
||||||
|
// if (!req.session.user_id) {
|
||||||
|
// if (req.cookies && req.cookies.login) {
|
||||||
|
// let token = await LoginToken.findOne({ token: req.cookies.login, valid: true });
|
||||||
|
// if (await CheckToken(token, true)) {
|
||||||
|
// req.session.user_id = token.user.toString();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (req.cookies && req.cookies.special) {
|
||||||
|
// let token = await LoginToken.findOne({ token: req.cookies.special, valid: true });
|
||||||
|
// if (await CheckToken(token, true)) {
|
||||||
|
// req.session.user_id = token.user.toString();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (req.session.user_id) {
|
||||||
|
req.user = await User.findById(req.session.user_id);
|
||||||
|
req.isAdmin = req.user.admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"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 /* Enables experimental support for ES7 decorators. */,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules/"],
|
"exclude": ["node_modules/"],
|
||||||
"files": ["src/express.d.ts"],
|
"files": ["src/express.d.ts"],
|
||||||
|
@ -2,12 +2,18 @@
|
|||||||
"name": "@hibas123/openauth-views-v2",
|
"name": "@hibas123/openauth-views-v2",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.7",
|
||||||
"@rollup/plugin-html": "^1.0.2",
|
"@rollup/plugin-html": "^1.0.2",
|
||||||
"@rollup/plugin-image": "^3.0.2",
|
"@rollup/plugin-image": "^3.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"@tsconfig/svelte": "^4.0.1",
|
"@tsconfig/svelte": "^4.0.1",
|
||||||
"@types/cleave.js": "^1.4.7",
|
"@types/cleave.js": "^1.4.7",
|
||||||
"esbuild": "^0.17.15",
|
"autoprefixer": "^10.4.14",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"cssnano": "^6.0.0",
|
||||||
|
"esbuild": "^0.17.16",
|
||||||
|
"flowbite": "^1.6.5",
|
||||||
|
"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",
|
||||||
@ -20,7 +26,8 @@
|
|||||||
"rollup-plugin-visualizer": "^5.9.0",
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
"svelte": "^3.58.0",
|
"svelte": "^3.58.0",
|
||||||
"svelte-preprocess": "^5.0.3",
|
"svelte-preprocess": "^5.0.3",
|
||||||
"typescript": "^5.0.3"
|
"tailwindcss": "^3.3.1",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
@ -28,9 +35,13 @@
|
|||||||
"dev": "rollup -c rollup.config.mjs -w"
|
"dev": "rollup -c rollup.config.mjs -w"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hibas123/openauth-internalapi": "workspace:^",
|
||||||
"@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",
|
||||||
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [],
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
cssnano: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,7 @@ import { visualizer } from "rollup-plugin-visualizer";
|
|||||||
import postcss from "rollup-plugin-postcss";
|
import postcss from "rollup-plugin-postcss";
|
||||||
import livereload from "rollup-plugin-livereload";
|
import livereload from "rollup-plugin-livereload";
|
||||||
import sveltePreprocess from "svelte-preprocess";
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
|
||||||
const VIEWS = ["home", "login", "popup", "user"];
|
const VIEWS = ["home", "login", "popup", "user"];
|
||||||
|
|
||||||
@ -66,22 +67,11 @@ const htmlTemplate = ({ attributes, meta, files, publicPath, title }) => {
|
|||||||
export default VIEWS.map((view) => ({
|
export default VIEWS.map((view) => ({
|
||||||
input: `src/pages/${view}/main.ts`,
|
input: `src/pages/${view}/main.ts`,
|
||||||
output: [
|
output: [
|
||||||
dev
|
{
|
||||||
? {
|
file: `build/${view}/bundle.min.js`,
|
||||||
file: `build/${view}/bundle.js`,
|
format: "es",
|
||||||
format: "iife",
|
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
name: view,
|
name: view,
|
||||||
}
|
|
||||||
: {
|
|
||||||
file: `build/${view}/bundle.min.js`,
|
|
||||||
format: "iife",
|
|
||||||
name: view,
|
|
||||||
plugins: [
|
|
||||||
esbuild({
|
|
||||||
minify: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -89,7 +79,8 @@ export default VIEWS.map((view) => ({
|
|||||||
emitCss: true,
|
emitCss: true,
|
||||||
preprocess: sveltePreprocess({}),
|
preprocess: sveltePreprocess({}),
|
||||||
}),
|
}),
|
||||||
esbuild({ sourceMap: dev }),
|
commonjs(),
|
||||||
|
esbuild({ sourceMap: dev, minify: true }),
|
||||||
html({
|
html({
|
||||||
title: view,
|
title: view,
|
||||||
attributes: {
|
attributes: {
|
||||||
@ -105,8 +96,8 @@ export default VIEWS.map((view) => ({
|
|||||||
}),
|
}),
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
dedupe: ["svelte"],
|
|
||||||
exportConditions: ["svelte"],
|
exportConditions: ["svelte"],
|
||||||
|
extensions: [".svelte"],
|
||||||
}),
|
}),
|
||||||
image(),
|
image(),
|
||||||
sizes(),
|
sizes(),
|
||||||
|
@ -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>
|
|
||||||
|
33
Frontend/src/components/MainNavbar.svelte
Normal file
33
Frontend/src/components/MainNavbar.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
NavBrand,
|
||||||
|
NavHamburger,
|
||||||
|
NavLi,
|
||||||
|
NavUl,
|
||||||
|
Navbar,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
|
||||||
|
export let sidebarOpen: boolean;
|
||||||
|
export let sidebarOpenVisible: boolean;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navbar let:hidden let:toggle color="form">
|
||||||
|
{#if sidebarOpenVisible}
|
||||||
|
<NavHamburger on:click={() => (sidebarOpen = !sidebarOpen)} />
|
||||||
|
{/if}
|
||||||
|
<NavBrand href="/">
|
||||||
|
<span
|
||||||
|
class="self-center whitespace-nowrap text-xl font-semibold dark:text-white"
|
||||||
|
>
|
||||||
|
OpenAuth
|
||||||
|
</span>
|
||||||
|
</NavBrand>
|
||||||
|
<NavHamburger on:click={toggle} />
|
||||||
|
<NavUl {hidden}>
|
||||||
|
<NavLi href="/" active={true}>Home</NavLi>
|
||||||
|
<NavLi href="/user">User</NavLi>
|
||||||
|
<!-- <NavLi href="/services">Services</NavLi>
|
||||||
|
<NavLi href="/pricing">Pricing</NavLi>
|
||||||
|
<NavLi href="/contact">Contact</NavLi> -->
|
||||||
|
</NavUl>
|
||||||
|
</Navbar>
|
@ -28,6 +28,7 @@ body {
|
|||||||
|
|
||||||
.group {
|
.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;
|
||||||
|
39
Frontend/src/helper/api.ts
Normal file
39
Frontend/src/helper/api.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Client } from "@hibas123/openauth-internalapi";
|
||||||
|
import request, { RequestError } from "./request";
|
||||||
|
|
||||||
|
const provider = new Client.ServiceProvider((data) => {
|
||||||
|
fetch("/api/jrpc", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
else throw new Error(res.statusText);
|
||||||
|
}).then(res => {
|
||||||
|
provider.onPacket(res);
|
||||||
|
}).catch(err => {
|
||||||
|
provider.onPacket({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: data.method,
|
||||||
|
id: data.id,
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: err.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const InternalAPI = {
|
||||||
|
Account: new Client.AccountService(provider),
|
||||||
|
Security: new Client.SecurityService(provider),
|
||||||
|
TwoFactor: new Client.TFAService(provider),
|
||||||
|
Login: new Client.LoginService(provider),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InternalAPI;
|
||||||
|
|
||||||
|
(window as any).InternalAPI = InternalAPI;
|
@ -2,6 +2,15 @@ import { getCookie } from "./cookie";
|
|||||||
|
|
||||||
const baseURL = "";
|
const baseURL = "";
|
||||||
|
|
||||||
|
export class RequestError extends Error {
|
||||||
|
response: any;
|
||||||
|
constructor(message: string, response: any) {
|
||||||
|
super(message);
|
||||||
|
this.name = "RequestError";
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function request(
|
export default async function request(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
parameters: { [key: string]: string } = {},
|
parameters: { [key: string]: string } = {},
|
||||||
@ -46,7 +55,7 @@ export default async function request(
|
|||||||
);
|
);
|
||||||
window.location.href = `/login?state=${state}&base64=true`;
|
window.location.href = `/login?state=${state}&base64=true`;
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(data.error));
|
return Promise.reject(new RequestError(data.error, data));
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
68
Frontend/src/main.css
Normal file
68
Frontend/src/main.css
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* material-icons-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||||
|
font-family: "Material Icons";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url("/static/material-icons-v140-latin-regular.woff2") format("woff2"),
|
||||||
|
/* Chrome 36+, Opera 23+, Firefox 39+ */
|
||||||
|
url("/static/material-icons-v140-latin-regular.woff") format("woff"); /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* material-icons-outlined-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||||
|
font-family: "Material Icons Outlined";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url("/static/material-icons-outlined-v109-latin-regular.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Chrome 36+, Opera 23+, Firefox 39+ */
|
||||||
|
url("/static/material-icons-outlined-v109-latin-regular.woff")
|
||||||
|
format("woff"); /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-family: "Material Icons";
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px; /* Preferred icon size */
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
|
||||||
|
/* Support for all WebKit browsers. */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
/* Support for Safari and Chrome. */
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
|
||||||
|
/* Support for Firefox. */
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
/* Support for IE. */
|
||||||
|
font-feature-settings: "liga";
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons-outlined {
|
||||||
|
font-family: "Material Icons Outlined";
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-feature-settings: "liga";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
@ -1,18 +1,3 @@
|
|||||||
<style>
|
|
||||||
.main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style: none;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li > a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<h1>Home Page</h1>
|
<h1>Home Page</h1>
|
||||||
|
|
||||||
@ -30,15 +15,19 @@
|
|||||||
</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>
|
||||||
|
.main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,124 +1,34 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import {} from "flowbite-svelte";
|
||||||
|
|
||||||
|
import { LoginState } from "@hibas123/openauth-internalapi";
|
||||||
import Theme from "../../components/theme";
|
import 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>
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Api from "./api.ts";
|
|
||||||
|
|
||||||
let error;
|
|
||||||
let password = "";
|
|
||||||
let username = Api.getUsername();
|
|
||||||
|
|
||||||
const states = {
|
|
||||||
username: 1,
|
|
||||||
password: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = states.username;
|
|
||||||
|
|
||||||
let salt;
|
|
||||||
|
|
||||||
export let setLoading;
|
|
||||||
export let next;
|
|
||||||
|
|
||||||
async function buttonClick() {
|
|
||||||
setLoading(true);
|
|
||||||
if (state === states.username) {
|
|
||||||
let res = await Api.setUsername(username);
|
|
||||||
if (res.error) {
|
|
||||||
error = res.error;
|
|
||||||
} else {
|
|
||||||
state = states.password;
|
|
||||||
error = undefined;
|
|
||||||
}
|
|
||||||
} else if (state === states.password) {
|
|
||||||
let res = await Api.setPassword(password);
|
|
||||||
if (res.error) {
|
|
||||||
error = res.error;
|
|
||||||
} else {
|
|
||||||
error = undefined;
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.error {
|
|
||||||
color: var(--error);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide-button {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{#if state === states.username}
|
|
||||||
<h3>Enter your Username or your E-Mail Address</h3>
|
|
||||||
<div class="floating group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autocomplete="username"
|
|
||||||
autofocus
|
|
||||||
bind:value={username} />
|
|
||||||
<span class="highlight" />
|
|
||||||
<span class="bar" />
|
|
||||||
<label>Username or E-Mail</label>
|
|
||||||
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<h3>Enter password for {username}</h3>
|
|
||||||
<div class="floating group">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
autocomplete="password"
|
|
||||||
autofocus
|
|
||||||
bind:value={password} />
|
|
||||||
<span class="highlight" />
|
|
||||||
<span class="bar" />
|
|
||||||
<label>Password</label>
|
|
||||||
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button class="btn btn-primary wide-button" on:click={buttonClick}>Next</button>
|
|
16
Frontend/src/pages/login/Error.svelte
Normal file
16
Frontend/src/pages/login/Error.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import loginState from "./state";
|
||||||
|
|
||||||
|
let { state } = loginState;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $state.error}
|
||||||
|
<div class="error">{$state.error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error {
|
||||||
|
color: var(--error);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
30
Frontend/src/pages/login/Password.svelte
Normal file
30
Frontend/src/pages/login/Password.svelte
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
|
||||||
|
let password: string = "";
|
||||||
|
export let username: string;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3>Enter the password for {username}</h3>
|
||||||
|
<div class="floating group">
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="password"
|
||||||
|
autofocus
|
||||||
|
bind:value={password}
|
||||||
|
/>
|
||||||
|
<span class="highlight" />
|
||||||
|
<span class="bar" />
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<Error />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-wide"
|
||||||
|
on:click={() => dispatch("password", password)}>Next</button
|
||||||
|
>
|
@ -1,8 +1,8 @@
|
|||||||
<script>
|
<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>
|
21
Frontend/src/pages/login/TF/TOTP.svelte
Normal file
21
Frontend/src/pages/login/TF/TOTP.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Error from "../Error.svelte";
|
||||||
|
import loginState from "../state";
|
||||||
|
import CodeInput from "./CodeInput.svelte";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let name: string;
|
||||||
|
|
||||||
|
let code: string = "";
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
loginState.useTOTP(id, code);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3>TOTP {name}</h3>
|
||||||
|
<CodeInput bind:value={code} length={6} />
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-primary btn-wide" on:click={send}> Send </button>
|
||||||
|
</div>
|
28
Frontend/src/pages/login/TF/WebAuthn.svelte
Normal file
28
Frontend/src/pages/login/TF/WebAuthn.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Error from "../Error.svelte";
|
||||||
|
import loginState from "../state";
|
||||||
|
import { startAuthentication } from "@simplewebauthn/browser";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
|
||||||
|
async function doAuth() {
|
||||||
|
let challenge = await loginState.getWebAuthnChallenge(id);
|
||||||
|
try {
|
||||||
|
loginState.setLoading(true);
|
||||||
|
let result = await startAuthentication(JSON.parse(challenge));
|
||||||
|
await loginState.useWebAuthn(id, result);
|
||||||
|
} catch (e) {
|
||||||
|
loginState.setError(e.message);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
loginState.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
doAuth();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Error />
|
114
Frontend/src/pages/login/TwoFactor.svelte
Normal file
114
Frontend/src/pages/login/TwoFactor.svelte
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import loginState from "./state";
|
||||||
|
import Icon from "./icons/Icon.svelte";
|
||||||
|
import { TFAType } from "@hibas123/openauth-internalapi";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Totp from "./TF/TOTP.svelte";
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
import WebAuthn from "./TF/WebAuthn.svelte";
|
||||||
|
|
||||||
|
let { state } = loginState;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let selected = undefined;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($state.requireTwoFactor?.length == 1) {
|
||||||
|
selected = $state.requireTwoFactor[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeIconMap = {
|
||||||
|
[TFAType.TOTP]: "Authenticator",
|
||||||
|
[TFAType.BACKUP_CODE]: "BackupCode",
|
||||||
|
[TFAType.WEBAUTHN]: "SecurityKey",
|
||||||
|
[TFAType.APP_ALLOW]: "AppPush",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !selected}
|
||||||
|
<h3>Choose your 2FA method</h3>
|
||||||
|
<ul>
|
||||||
|
{#each $state.requireTwoFactor ?? [] as method}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<li on:click={() => (selected = method)}>
|
||||||
|
<div class="icon">
|
||||||
|
<Icon icon_name={typeIconMap[method.tfatype]} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name">{method.name}</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<Error />
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
{#if selected.tfatype == TFAType.TOTP}
|
||||||
|
<Totp id={selected.id} name={selected.name} />
|
||||||
|
{:else if selected.tfatype == TFAType.BACKUP_CODE}
|
||||||
|
backup
|
||||||
|
{:else if selected.tfatype == TFAType.WEBAUTHN}
|
||||||
|
<WebAuthn id={selected.id} />
|
||||||
|
{:else if selected.tfatype == TFAType.APP_ALLOW}
|
||||||
|
appallow
|
||||||
|
{:else}
|
||||||
|
<p>Unknown 2FA type</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
class="to-list"
|
||||||
|
href="# "
|
||||||
|
on:click={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
loginState.setError(undefined);
|
||||||
|
selected = undefined;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Choose another Method
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
border-top: 1px grey solid;
|
||||||
|
padding: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:hover {
|
||||||
|
background-color: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:first-child {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-left: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-size: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-list {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,104 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Api, { TFATypes } from "./api.ts";
|
|
||||||
import Icon from "./icons/Icon.svelte";
|
|
||||||
|
|
||||||
import OTCTwoFactor from "./twofactors/otc.svelte";
|
|
||||||
import PushTwoFactor from "./twofactors/push.svelte";
|
|
||||||
import U2FTwoFactor from "./twofactors/u2f.svelte";
|
|
||||||
|
|
||||||
const states = {
|
|
||||||
list: 1,
|
|
||||||
twofactor: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
function getIcon(tf) {
|
|
||||||
switch (tf.type) {
|
|
||||||
case TFATypes.OTC:
|
|
||||||
return "Authenticator";
|
|
||||||
case TFATypes.BACKUP_CODE:
|
|
||||||
return "BackupCode";
|
|
||||||
case TFATypes.U2F:
|
|
||||||
return "SecurityKey";
|
|
||||||
case TFATypes.APP_ALLOW:
|
|
||||||
return "AppPush";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let twofactors = Api.twofactor.map(tf => {
|
|
||||||
return {
|
|
||||||
...tf,
|
|
||||||
icon: getIcon(tf)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let state = states.list;
|
|
||||||
|
|
||||||
let twofactor = undefined;
|
|
||||||
twofactor = twofactors[0];
|
|
||||||
$: console.log(twofactor);
|
|
||||||
|
|
||||||
function onFinish(res) {
|
|
||||||
if (res) finish();
|
|
||||||
else twofactor = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let finish;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
border-top: 1px grey solid;
|
|
||||||
padding: 1em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:first-child {
|
|
||||||
border-top: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
float: left;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin-left: 48px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if !twofactor}
|
|
||||||
<h3>Select your Authentication method:</h3>
|
|
||||||
<ul>
|
|
||||||
{#each twofactors as tf}
|
|
||||||
<li on:click={() => (twofactor = tf)}>
|
|
||||||
<div class="icon">
|
|
||||||
<Icon icon_name={tf.icon} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="name">{tf.name}</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{:else if twofactor.type === TFATypes.OTC}
|
|
||||||
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={true} />
|
|
||||||
{:else if twofactor.type === TFATypes.BACKUP_CODE}
|
|
||||||
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={false} />
|
|
||||||
{:else if twofactor.type === TFATypes.U2F}
|
|
||||||
<U2FTwoFactor id={twofactor.id} finish={onFinish} />
|
|
||||||
{:else if twofactor.type === TFATypes.APP_ALLOW}
|
|
||||||
<PushTwoFactor id={twofactor.id} finish={onFinish} />
|
|
||||||
{:else}
|
|
||||||
<div>Invalid TwoFactor Method!</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
</div>
|
|
29
Frontend/src/pages/login/Username.svelte
Normal file
29
Frontend/src/pages/login/Username.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import Error from "./Error.svelte";
|
||||||
|
|
||||||
|
let username: string = "";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3>Enter your Username or your E-Mail Address</h3>
|
||||||
|
<div class="floating group">
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
autofocus
|
||||||
|
bind:value={username}
|
||||||
|
/>
|
||||||
|
<span class="highlight" />
|
||||||
|
<span class="bar" />
|
||||||
|
<label for="username">Username or E-Mail</label>
|
||||||
|
<Error />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-wide"
|
||||||
|
on:click={() => dispatch("username", username)}>Next</button
|
||||||
|
>
|
@ -1,182 +0,0 @@
|
|||||||
import request from "../../helper/request";
|
|
||||||
import sha from "../../helper/sha512";
|
|
||||||
import { setCookie, getCookie } from "../../helper/cookie";
|
|
||||||
|
|
||||||
export interface TwoFactor {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: TFATypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TFATypes {
|
|
||||||
OTC,
|
|
||||||
BACKUP_CODE,
|
|
||||||
U2F,
|
|
||||||
APP_ALLOW,
|
|
||||||
}
|
|
||||||
|
|
||||||
// const Api = {
|
|
||||||
// // twofactor: [{
|
|
||||||
// // id: "1",
|
|
||||||
// // name: "Backup Codes",
|
|
||||||
// // type: TFATypes.BACKUP_CODE
|
|
||||||
// // }, {
|
|
||||||
// // id: "2",
|
|
||||||
// // name: "YubiKey",
|
|
||||||
// // type: TFATypes.U2F
|
|
||||||
// // }, {
|
|
||||||
// // id: "3",
|
|
||||||
// // name: "Authenticator",
|
|
||||||
// // type: TFATypes.OTC
|
|
||||||
// // }] as TwoFactor[],
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
export interface IToken {
|
|
||||||
token: string;
|
|
||||||
expires: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeid(length) {
|
|
||||||
var result = "";
|
|
||||||
var characters =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
var charactersLength = characters.length;
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Api {
|
|
||||||
static salt: string;
|
|
||||||
static login: IToken;
|
|
||||||
static special: IToken;
|
|
||||||
static username: string;
|
|
||||||
|
|
||||||
static twofactor: any[];
|
|
||||||
|
|
||||||
static getUsername() {
|
|
||||||
return this.username || getCookie("username");
|
|
||||||
}
|
|
||||||
|
|
||||||
static async setUsername(
|
|
||||||
username: string
|
|
||||||
): Promise<{ error: string | undefined }> {
|
|
||||||
return request(
|
|
||||||
"/api/user/login",
|
|
||||||
{
|
|
||||||
type: "username",
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
"POST"
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
this.salt = res.salt;
|
|
||||||
this.username = username;
|
|
||||||
return {
|
|
||||||
error: undefined,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
let error = err.message;
|
|
||||||
return { error };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async setPassword(
|
|
||||||
password: string
|
|
||||||
): Promise<{ error: string | undefined; twofactor?: any }> {
|
|
||||||
const date = new Date().valueOf();
|
|
||||||
let pw = sha(sha(this.salt + password) + date.toString());
|
|
||||||
return request(
|
|
||||||
"/api/user/login",
|
|
||||||
{
|
|
||||||
type: "password",
|
|
||||||
},
|
|
||||||
"POST",
|
|
||||||
{
|
|
||||||
username: this.username,
|
|
||||||
password: pw,
|
|
||||||
date,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(({ login, special, tfa }) => {
|
|
||||||
this.login = login;
|
|
||||||
this.special = special;
|
|
||||||
|
|
||||||
if (tfa && Array.isArray(tfa) && tfa.length > 0)
|
|
||||||
this.twofactor = tfa;
|
|
||||||
else this.twofactor = undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
error: undefined,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
let error = err.message;
|
|
||||||
return { error };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static gettok() {
|
|
||||||
return {
|
|
||||||
login: this.login.token,
|
|
||||||
special: this.special.token,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static async sendBackup(id: string, code: string) {
|
|
||||||
return request("/api/user/twofactor/backup", this.gettok(), "PUT", {
|
|
||||||
code,
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.then(({ login_exp, special_exp }) => {
|
|
||||||
this.login.expires = login_exp;
|
|
||||||
this.special.expires = special_exp;
|
|
||||||
return {};
|
|
||||||
})
|
|
||||||
.catch((err) => ({ error: err.message }));
|
|
||||||
}
|
|
||||||
|
|
||||||
static async sendOTC(id: string, code: string) {
|
|
||||||
return request("/api/user/twofactor/otc", this.gettok(), "PUT", {
|
|
||||||
code,
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.then(({ login_exp, special_exp }) => {
|
|
||||||
this.login.expires = login_exp;
|
|
||||||
this.special.expires = special_exp;
|
|
||||||
return {};
|
|
||||||
})
|
|
||||||
.catch((error) => ({ error: error.message }));
|
|
||||||
}
|
|
||||||
|
|
||||||
static finish() {
|
|
||||||
let d = new Date();
|
|
||||||
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
|
|
||||||
setCookie("username", this.username, d.toUTCString());
|
|
||||||
|
|
||||||
setCookie(
|
|
||||||
"login",
|
|
||||||
this.login.token,
|
|
||||||
new Date(this.login.expires).toUTCString()
|
|
||||||
);
|
|
||||||
setCookie(
|
|
||||||
"special",
|
|
||||||
this.special.token,
|
|
||||||
new Date(this.special.expires).toUTCString()
|
|
||||||
);
|
|
||||||
|
|
||||||
let url = new URL(window.location.href);
|
|
||||||
let state = url.searchParams.get("state");
|
|
||||||
let red = "/";
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
let base64 = url.searchParams.get("base64");
|
|
||||||
if (base64) red = atob(state);
|
|
||||||
else red = state;
|
|
||||||
}
|
|
||||||
setTimeout(() => (window.location.href = red), 200);
|
|
||||||
}
|
|
||||||
}
|
|
183
Frontend/src/pages/login/state.ts
Normal file
183
Frontend/src/pages/login/state.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import type { LoginState } from "@hibas123/openauth-internalapi";
|
||||||
|
import { derived, get, writable } from "svelte/store";
|
||||||
|
import InternalAPI from "../../helper/api";
|
||||||
|
import sha from "../../helper/sha512";
|
||||||
|
|
||||||
|
interface LocalLoginState extends LoginState {
|
||||||
|
loading: boolean;
|
||||||
|
error?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginStore {
|
||||||
|
state = writable<LocalLoginState>({
|
||||||
|
username: undefined,
|
||||||
|
password: false,
|
||||||
|
passwordSalt: undefined,
|
||||||
|
requireTwoFactor: [],
|
||||||
|
success: false,
|
||||||
|
loading: true,
|
||||||
|
error: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
isFinished = derived(this.state, $state => $state.success);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.state.subscribe((state) => {
|
||||||
|
if (state.success) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.finish();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(loading: boolean) {
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
loading,
|
||||||
|
error: loading ? undefined : current.error,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(error: string) {
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
error,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getState() {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
let state = await InternalAPI.Login.GetState();
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
...state,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUsername(username: string) {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
let state = await InternalAPI.Login.Start(username);
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
...state,
|
||||||
|
username
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(password: string) {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
const date = new Date().valueOf();
|
||||||
|
let salt = get(this.state).passwordSalt
|
||||||
|
let pw = sha(sha(salt + password) + date.toString());
|
||||||
|
|
||||||
|
let state = await InternalAPI.Login.UsePassword(pw, date);
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
...state,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async useTOTP(id: string, code: string) {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
let state = await InternalAPI.Login.UseTOTP(id, code);
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
...state,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async useBackupCode(id: string, code: string) {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
let state = await InternalAPI.Login.UseBackupCode(id, code);
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
...state,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWebAuthnChallenge(id: string) {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
let challenge = await InternalAPI.Login.GetWebAuthnChallenge(id);
|
||||||
|
return challenge;
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async useWebAuthn(id: string, response: any) {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
let state = await InternalAPI.Login.UseWebAuthn(id, JSON.stringify(response));
|
||||||
|
this.state.update(current => ({
|
||||||
|
...current,
|
||||||
|
...state,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
this.setError(err.message);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async finish() {
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
let state = url.searchParams.get("state");
|
||||||
|
let red = "/";
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
let base64 = url.searchParams.get("base64");
|
||||||
|
if (base64) red = atob(state);
|
||||||
|
else red = state;
|
||||||
|
}
|
||||||
|
setTimeout(() => (window.location.href = red), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginState = new LoginStore();
|
||||||
|
|
||||||
|
export default loginState;
|
@ -1,50 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ToList from "./toList.svelte";
|
|
||||||
import Api from "../api.ts";
|
|
||||||
import CodeInput from "./codeInput.svelte";
|
|
||||||
|
|
||||||
let error = "";
|
|
||||||
let code = "";
|
|
||||||
export let finish;
|
|
||||||
export let id;
|
|
||||||
|
|
||||||
export let otc = false;
|
|
||||||
let title = otc ? "One Time Code (OTC)" : "Backup Code";
|
|
||||||
let length = otc ? 6 : 8;
|
|
||||||
|
|
||||||
async function sendCode() {
|
|
||||||
let c = code.replace(/\s+/g, "");
|
|
||||||
if (c.length < length) {
|
|
||||||
error = `Code must be ${length} digits long!`;
|
|
||||||
} else {
|
|
||||||
error = "";
|
|
||||||
let res;
|
|
||||||
if (otc) res = await Api.sendOTC(id, c);
|
|
||||||
else res = await Api.sendBackup(id, c);
|
|
||||||
if (res.error) error = res.error;
|
|
||||||
else finish(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-next {
|
|
||||||
margin: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h3>{title}</h3>
|
|
||||||
|
|
||||||
<CodeInput bind:value={code} label="Code" {error} {length} />
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<ToList {finish} />
|
|
||||||
<button class="btn btn-primary btn-next" on:click={sendCode}> Send </button>
|
|
||||||
</div>
|
|
@ -1,389 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ToList from "./toList.svelte";
|
|
||||||
|
|
||||||
let error = "";
|
|
||||||
let code = "";
|
|
||||||
export let device = "Handy01";
|
|
||||||
// export let deviceId = "";
|
|
||||||
|
|
||||||
export let finish;
|
|
||||||
|
|
||||||
async function requestPush() {
|
|
||||||
// Push Request
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.error {
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 {
|
|
||||||
position: relative;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
margin: 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 .wBall {
|
|
||||||
position: absolute;
|
|
||||||
width: 53px;
|
|
||||||
height: 53px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(225deg);
|
|
||||||
-o-transform: rotate(225deg);
|
|
||||||
-ms-transform: rotate(225deg);
|
|
||||||
-webkit-transform: rotate(225deg);
|
|
||||||
-moz-transform: rotate(225deg);
|
|
||||||
animation: orbit 5.7425s infinite;
|
|
||||||
-o-animation: orbit 5.7425s infinite;
|
|
||||||
-ms-animation: orbit 5.7425s infinite;
|
|
||||||
-webkit-animation: orbit 5.7425s infinite;
|
|
||||||
-moz-animation: orbit 5.7425s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 .wBall .wInnerBall {
|
|
||||||
position: absolute;
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
background: rgb(0, 140, 255);
|
|
||||||
left: 0px;
|
|
||||||
top: 0px;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 #wBall_1 {
|
|
||||||
animation-delay: 1.256s;
|
|
||||||
-o-animation-delay: 1.256s;
|
|
||||||
-ms-animation-delay: 1.256s;
|
|
||||||
-webkit-animation-delay: 1.256s;
|
|
||||||
-moz-animation-delay: 1.256s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 #wBall_2 {
|
|
||||||
animation-delay: 0.243s;
|
|
||||||
-o-animation-delay: 0.243s;
|
|
||||||
-ms-animation-delay: 0.243s;
|
|
||||||
-webkit-animation-delay: 0.243s;
|
|
||||||
-moz-animation-delay: 0.243s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 #wBall_3 {
|
|
||||||
animation-delay: 0.5065s;
|
|
||||||
-o-animation-delay: 0.5065s;
|
|
||||||
-ms-animation-delay: 0.5065s;
|
|
||||||
-webkit-animation-delay: 0.5065s;
|
|
||||||
-moz-animation-delay: 0.5065s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 #wBall_4 {
|
|
||||||
animation-delay: 0.7495s;
|
|
||||||
-o-animation-delay: 0.7495s;
|
|
||||||
-ms-animation-delay: 0.7495s;
|
|
||||||
-webkit-animation-delay: 0.7495s;
|
|
||||||
-moz-animation-delay: 0.7495s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.windows8 #wBall_5 {
|
|
||||||
animation-delay: 1.003s;
|
|
||||||
-o-animation-delay: 1.003s;
|
|
||||||
-ms-animation-delay: 1.003s;
|
|
||||||
-webkit-animation-delay: 1.003s;
|
|
||||||
-moz-animation-delay: 1.003s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes orbit {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 99;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
7% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(300deg);
|
|
||||||
animation-timing-function: linear;
|
|
||||||
origin: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(410deg);
|
|
||||||
animation-timing-function: ease-in-out;
|
|
||||||
origin: 7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
39% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(645deg);
|
|
||||||
animation-timing-function: linear;
|
|
||||||
origin: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(770deg);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
origin: 39%;
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(900deg);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
origin: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
76% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-o-keyframes orbit {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 99;
|
|
||||||
-o-transform: rotate(180deg);
|
|
||||||
-o-animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
7% {
|
|
||||||
opacity: 1;
|
|
||||||
-o-transform: rotate(300deg);
|
|
||||||
-o-animation-timing-function: linear;
|
|
||||||
-o-origin: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
-o-transform: rotate(410deg);
|
|
||||||
-o-animation-timing-function: ease-in-out;
|
|
||||||
-o-origin: 7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
39% {
|
|
||||||
opacity: 1;
|
|
||||||
-o-transform: rotate(645deg);
|
|
||||||
-o-animation-timing-function: linear;
|
|
||||||
-o-origin: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
-o-transform: rotate(770deg);
|
|
||||||
-o-animation-timing-function: ease-out;
|
|
||||||
-o-origin: 39%;
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
opacity: 1;
|
|
||||||
-o-transform: rotate(900deg);
|
|
||||||
-o-animation-timing-function: ease-out;
|
|
||||||
-o-origin: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
76% {
|
|
||||||
opacity: 0;
|
|
||||||
-o-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-o-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-ms-keyframes orbit {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 99;
|
|
||||||
-ms-transform: rotate(180deg);
|
|
||||||
-ms-animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
7% {
|
|
||||||
opacity: 1;
|
|
||||||
-ms-transform: rotate(300deg);
|
|
||||||
-ms-animation-timing-function: linear;
|
|
||||||
-ms-origin: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
-ms-transform: rotate(410deg);
|
|
||||||
-ms-animation-timing-function: ease-in-out;
|
|
||||||
-ms-origin: 7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
39% {
|
|
||||||
opacity: 1;
|
|
||||||
-ms-transform: rotate(645deg);
|
|
||||||
-ms-animation-timing-function: linear;
|
|
||||||
-ms-origin: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
-ms-transform: rotate(770deg);
|
|
||||||
-ms-animation-timing-function: ease-out;
|
|
||||||
-ms-origin: 39%;
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
opacity: 1;
|
|
||||||
-ms-transform: rotate(900deg);
|
|
||||||
-ms-animation-timing-function: ease-out;
|
|
||||||
-ms-origin: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
76% {
|
|
||||||
opacity: 0;
|
|
||||||
-ms-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-ms-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes orbit {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 99;
|
|
||||||
-webkit-transform: rotate(180deg);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
7% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: rotate(300deg);
|
|
||||||
-webkit-animation-timing-function: linear;
|
|
||||||
-webkit-origin: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: rotate(410deg);
|
|
||||||
-webkit-animation-timing-function: ease-in-out;
|
|
||||||
-webkit-origin: 7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
39% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: rotate(645deg);
|
|
||||||
-webkit-animation-timing-function: linear;
|
|
||||||
-webkit-origin: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: rotate(770deg);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
-webkit-origin: 39%;
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: rotate(900deg);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
-webkit-origin: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
76% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-moz-keyframes orbit {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 99;
|
|
||||||
-moz-transform: rotate(180deg);
|
|
||||||
-moz-animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
7% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: rotate(300deg);
|
|
||||||
-moz-animation-timing-function: linear;
|
|
||||||
-moz-origin: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: rotate(410deg);
|
|
||||||
-moz-animation-timing-function: ease-in-out;
|
|
||||||
-moz-origin: 7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
39% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: rotate(645deg);
|
|
||||||
-moz-animation-timing-function: linear;
|
|
||||||
-moz-origin: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: rotate(770deg);
|
|
||||||
-moz-animation-timing-function: ease-out;
|
|
||||||
-moz-origin: 39%;
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: rotate(900deg);
|
|
||||||
-moz-animation-timing-function: ease-out;
|
|
||||||
-moz-origin: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
76% {
|
|
||||||
opacity: 0;
|
|
||||||
-moz-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-moz-transform: rotate(900deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h3>SMS</h3>
|
|
||||||
|
|
||||||
<p>A code was sent to your Device <b>{device}</b></p>
|
|
||||||
|
|
||||||
<div class="windows8">
|
|
||||||
<div class="wBall" id="wBall_1">
|
|
||||||
<div class="wInnerBall" />
|
|
||||||
</div>
|
|
||||||
<div class="wBall" id="wBall_2">
|
|
||||||
<div class="wInnerBall" />
|
|
||||||
</div>
|
|
||||||
<div class="wBall" id="wBall_3">
|
|
||||||
<div class="wInnerBall" />
|
|
||||||
</div>
|
|
||||||
<div class="wBall" id="wBall_4">
|
|
||||||
<div class="wInnerBall" />
|
|
||||||
</div>
|
|
||||||
<div class="wBall" id="wBall_5">
|
|
||||||
<div class="wInnerBall" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="error">{error}</div>
|
|
||||||
<ToList {finish} />
|
|
@ -1,49 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ToList from "./toList.svelte";
|
|
||||||
|
|
||||||
const states = {
|
|
||||||
approve: 1,
|
|
||||||
enter: 2,
|
|
||||||
};
|
|
||||||
let state = states.approve;
|
|
||||||
|
|
||||||
let error = "";
|
|
||||||
let code = "";
|
|
||||||
export let number = "+4915...320";
|
|
||||||
//export let finish;
|
|
||||||
|
|
||||||
function validateCode() {}
|
|
||||||
|
|
||||||
function sendCode() {
|
|
||||||
// Send request to Server
|
|
||||||
state = states.enter;
|
|
||||||
//finish()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--error: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h3>SMS</h3>
|
|
||||||
{#if state === states.approve}
|
|
||||||
<p>Send SMS to {number}</p>
|
|
||||||
<button class="btn btn-primary" on:click={sendCode}>Send</button>
|
|
||||||
{:else}
|
|
||||||
<p>A code was sent to you. Please enter</p>
|
|
||||||
<input type="number" placeholder="Code" bind:value={code} />
|
|
||||||
<button class="btn btn-primary" on:click={validateCode}>Send</button>
|
|
||||||
<br />
|
|
||||||
<a href="# " on:click|preventDefault={() => (state = states.approve)}>
|
|
||||||
Not received?
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
<div class="error">{error}</div>
|
|
||||||
|
|
||||||
<ToList {finish} />
|
|
@ -1,17 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let finish = () => {};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="# " on:click={evt => evt.preventDefault() || finish(false)}>
|
|
||||||
Choose another Method
|
|
||||||
</a>
|
|
||||||
</p>
|
|
@ -1,69 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ToList from "./toList.svelte";
|
|
||||||
|
|
||||||
export let finish;
|
|
||||||
|
|
||||||
const states = {
|
|
||||||
getChallenge: 0,
|
|
||||||
requestUser: 1,
|
|
||||||
sendChallenge: 2,
|
|
||||||
error: 3
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = states.getChallenge;
|
|
||||||
|
|
||||||
let error = "";
|
|
||||||
|
|
||||||
const onError = err => {
|
|
||||||
state = states.error;
|
|
||||||
error = err.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
let challenge;
|
|
||||||
|
|
||||||
async function requestUser() {
|
|
||||||
state = states.requestUser;
|
|
||||||
let res = await window.navigator.credentials.get({
|
|
||||||
publicKey: challenge
|
|
||||||
});
|
|
||||||
state = states.sendChallenge();
|
|
||||||
let r = res.response;
|
|
||||||
let data = encode({
|
|
||||||
authenticatorData: r.authenticatorData,
|
|
||||||
clientDataJSON: r.clientDataJSON,
|
|
||||||
signature: r.signature,
|
|
||||||
userHandle: r.userHandle
|
|
||||||
});
|
|
||||||
let { success } = fetch("https://localhost:8444/auth", {
|
|
||||||
body: data,
|
|
||||||
method: "POST"
|
|
||||||
}).then(res => res.json());
|
|
||||||
if (success) {
|
|
||||||
finish(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getChallenge() {
|
|
||||||
state = states.getChallenge;
|
|
||||||
challenge = await fetch("https://localhost:8444/auth")
|
|
||||||
.then(res => res.arrayBuffer())
|
|
||||||
.then(data => decode(MessagePack.Buffer.from(data)));
|
|
||||||
|
|
||||||
requestUser().catch(onError);
|
|
||||||
}
|
|
||||||
getChallenge().catch(onError);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--error: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h3>U2F Security Key</h3>
|
|
||||||
<h4>This Method is currently not supported. Please choose another one!</h4>
|
|
||||||
<ToList {finish} />
|
|
@ -84,9 +84,25 @@ async function onMessage(msg: MessageEvent<any>) {
|
|||||||
const url = new URL(msg.origin);
|
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);
|
||||||
|
@ -1,207 +1,42 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import AccountPage from "./Pages/Account.svelte";
|
import { onMount } from "svelte";
|
||||||
import SecurityPage from "./Pages/Security.svelte";
|
import MainNavbar from "../../components/MainNavbar.svelte";
|
||||||
import { slide, fade } from "svelte/transition";
|
import Sidebar from "./Sidebar.svelte";
|
||||||
|
import { CurrentPage } from "./nav";
|
||||||
|
import PersonalInfo from "./pages/PersonalInfo.svelte";
|
||||||
|
import Security from "./pages/Security.svelte";
|
||||||
|
|
||||||
const pages = [
|
let sidebarOpen = false;
|
||||||
{
|
let sidebarOpenVisible = false;
|
||||||
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() {
|
onMount(() => {
|
||||||
let pageid = window.location.hash.slice(1);
|
const unsub = CurrentPage.subscribe(() => {
|
||||||
return pages.find((e) => e.id === pageid) || pages[0];
|
sidebarOpen = false;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return unsub;
|
||||||
|
});
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<div class:loading class="root">
|
<div class="grid main-grid min-h-screen overflow-hidden">
|
||||||
<div class="app_container">
|
<div class="col-span-2">
|
||||||
<div class="header">
|
<MainNavbar bind:sidebarOpen bind:sidebarOpenVisible />
|
||||||
{#if sidebar_button}
|
</div>
|
||||||
<button on:click={() => (sidebar_active = !sidebar_active)}>
|
<div>
|
||||||
<svg
|
<Sidebar bind:sidebarOpen bind:sidebarOpenVisible />
|
||||||
id="Layer_1"
|
</div>
|
||||||
style="enable-background:new 0 0 32 32;"
|
<div class="overflow-auto p-4">
|
||||||
version="1.1"
|
{#if $CurrentPage == "personal-info"}
|
||||||
viewBox="0 0 32 32"
|
<PersonalInfo />
|
||||||
width="32px"
|
{:else if $CurrentPage == "security"}
|
||||||
xml:space="preserve"
|
<Security />
|
||||||
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}
|
{/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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
<div class="loader_container">
|
|
||||||
<div class="loader_box">
|
|
||||||
<div class="loader" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.loading {
|
.main-grid {
|
||||||
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;
|
grid-template-columns: auto 1fr;
|
||||||
}
|
grid-template-rows: 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>
|
</style>
|
||||||
|
19
Frontend/src/pages/user/Loading.svelte
Normal file
19
Frontend/src/pages/user/Loading.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Alert, Spinner } from "flowbite-svelte";
|
||||||
|
|
||||||
|
export let loading: boolean;
|
||||||
|
export let error: string | undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="h-full flex justify-center items-center">
|
||||||
|
<Spinner size={"16"} />
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<Alert color="red">
|
||||||
|
<span class="font-medium">Error occured!</span>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
{:else}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
@ -1,54 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let open;
|
|
||||||
export let active;
|
|
||||||
export let pages = [];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each pages as page}
|
|
||||||
<div
|
|
||||||
class={"item_container" + (page === active ? " active" : "")}
|
|
||||||
on:click={() => open(page.id)}
|
|
||||||
>
|
|
||||||
<div class="icon">
|
|
||||||
<img alt={page.title} src={page.icon} />
|
|
||||||
</div>
|
|
||||||
<h3 class="title">{page.title}</h3>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--rel-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item_container {
|
|
||||||
height: calc(var(--rel-size) * 5);
|
|
||||||
padding: var(--rel-size);
|
|
||||||
display: flex;
|
|
||||||
/* align-content: center; */
|
|
||||||
align-items: center;
|
|
||||||
/* justify-content: center; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
/* float: left; */
|
|
||||||
width: calc(var(--rel-size) * 3);
|
|
||||||
height: calc(var(--rel-size) * 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon > img {
|
|
||||||
width: calc(var(--rel-size) * 3);
|
|
||||||
height: calc(var(--rel-size) * 3);
|
|
||||||
stroke-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
/* margin: auto; */
|
|
||||||
margin-left: var(--rel-size);
|
|
||||||
/* height: 100%; */
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,192 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Box from "./Box.svelte";
|
|
||||||
import BoxItem from "./BoxItem.svelte";
|
|
||||||
import NextIcon from "./NextIcon.svelte";
|
|
||||||
|
|
||||||
import request from "../../../helper/request.ts";
|
|
||||||
|
|
||||||
export let loading = false;
|
|
||||||
let account_error = undefined;
|
|
||||||
let contact_error = undefined;
|
|
||||||
|
|
||||||
const genderMap = new Map();
|
|
||||||
genderMap.set(0, "None");
|
|
||||||
genderMap.set(1, "Male");
|
|
||||||
genderMap.set(2, "Female");
|
|
||||||
genderMap.set(3, "Other");
|
|
||||||
|
|
||||||
let name = "";
|
|
||||||
let gender = 0;
|
|
||||||
$: genderHuman = genderMap.get(gender) || "ERROR";
|
|
||||||
let birthday = undefined;
|
|
||||||
|
|
||||||
async function saveName() {
|
|
||||||
//TODO: implement
|
|
||||||
await load();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveGender() {
|
|
||||||
//TODO: implement
|
|
||||||
await load();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadProfile() {
|
|
||||||
try {
|
|
||||||
let { user } = await request(
|
|
||||||
"/api/user/account",
|
|
||||||
{},
|
|
||||||
"GET",
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
name = user.name;
|
|
||||||
// username = user.username;
|
|
||||||
gender = user.gender;
|
|
||||||
birthday = user.birthday
|
|
||||||
? new Date(user.birthday).toLocaleDateString()
|
|
||||||
: undefined;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
account_error = err.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let email = [];
|
|
||||||
let phone = [];
|
|
||||||
|
|
||||||
async function loadContact() {
|
|
||||||
try {
|
|
||||||
let { contact } = await request(
|
|
||||||
"/api/user/contact",
|
|
||||||
{},
|
|
||||||
"GET",
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
email = contact.mails.map((e) => e.mail);
|
|
||||||
phone = contact.phones.map((e) => e.phone);
|
|
||||||
contact_error = undefined;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
contact_error = err.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading = true;
|
|
||||||
await Promise.all([loadProfile(), loadContact()]);
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn {
|
|
||||||
background-color: var(--primary);
|
|
||||||
margin: auto 0;
|
|
||||||
margin-left: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container > *:first-child {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
background-color: unset;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
color: unset;
|
|
||||||
font-size: unset;
|
|
||||||
border-bottom: 1px solid #757575;
|
|
||||||
/* Firefox */
|
|
||||||
-moz-appearance: none;
|
|
||||||
/* Safari and Chrome */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
select > option {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper::after {
|
|
||||||
content: ">";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
right: 2rem;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 1rem;
|
|
||||||
transform: rotate(90deg) scaleY(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<h1>Profile</h1>
|
|
||||||
{#if account_error}
|
|
||||||
<p class="error">{account_error}</p>
|
|
||||||
{/if}
|
|
||||||
<BoxItem name="Name" value={name}>
|
|
||||||
<div class="input-container">
|
|
||||||
<div class="floating group">
|
|
||||||
<input
|
|
||||||
id="name-inp"
|
|
||||||
type="text"
|
|
||||||
autocomplete="username"
|
|
||||||
bind:value={name} />
|
|
||||||
<span class="highlight" />
|
|
||||||
<span class="bar" />
|
|
||||||
<label for="name-inp">Name</label>
|
|
||||||
</div>
|
|
||||||
<button class="btn" on:click={saveName}>Save</button>
|
|
||||||
</div>
|
|
||||||
</BoxItem>
|
|
||||||
<BoxItem name="Gender" value={genderHuman}>
|
|
||||||
<div class="input-container">
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<select bind:value={gender}>
|
|
||||||
<option value={1}>Male</option>
|
|
||||||
<option value={2}>Female</option>
|
|
||||||
<option value={3}>Other</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="btn" on:click={saveGender}>Save</button>
|
|
||||||
</div>
|
|
||||||
</BoxItem>
|
|
||||||
<BoxItem name="Birthday" value={birthday} />
|
|
||||||
<BoxItem name="Password" value="******" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<h1>Contact</h1>
|
|
||||||
{#if contact_error}
|
|
||||||
<p class="error">{contact_error}</p>
|
|
||||||
{/if}
|
|
||||||
<BoxItem name="E-Mail" value={email} noOpen={true} />
|
|
||||||
<BoxItem name="Phone" value={phone} noOpen={true} />
|
|
||||||
</Box>
|
|
@ -1,36 +0,0 @@
|
|||||||
<style>
|
|
||||||
.box {
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.30), 0 5px 4px rgba(0, 0, 0, 0.22);
|
|
||||||
padding: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box> :global(h1) {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #444444;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box> :global(div) {
|
|
||||||
padding: 16px;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box> :global(div):first-of-type {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 45rem) {
|
|
||||||
.box {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
@ -1,94 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { slide } from "svelte/transition";
|
|
||||||
import NextIcon from "./NextIcon.svelte";
|
|
||||||
export let name = "";
|
|
||||||
export let value = "";
|
|
||||||
export let noOpen = false;
|
|
||||||
export let open = false;
|
|
||||||
export let highlight = false;
|
|
||||||
|
|
||||||
function toggleOpen(ev) {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: calc(100% - var(--default-font-size) - 16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.values > div:first-child {
|
|
||||||
transform-origin: left;
|
|
||||||
transform: scale(0.95);
|
|
||||||
margin-right: 24px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values > div:nth-child(2) {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(svg) {
|
|
||||||
margin: auto 8px auto 8px;
|
|
||||||
height: var(--default-font-size);
|
|
||||||
min-width: var(--default-font-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0.1px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 45rem) {
|
|
||||||
.values {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values > div:first-child {
|
|
||||||
transform: unset;
|
|
||||||
flex-basis: 120px;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight-element {
|
|
||||||
background-color: #7bff003b;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="root" class:highlight-element={highlight}>
|
|
||||||
<div class="container" on:click={() => (open = !open)}>
|
|
||||||
<div class="values">
|
|
||||||
<div>{name}</div>
|
|
||||||
<div>
|
|
||||||
{#if Array.isArray(value)}
|
|
||||||
{#each value as v, i}
|
|
||||||
{v}
|
|
||||||
{#if i < value.length - 1}
|
|
||||||
<br />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{:else}{value}{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if !noOpen}
|
|
||||||
<NextIcon rotation={open ? -90 : 90} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if open && !noOpen}
|
|
||||||
<div class="body" transition:slide>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let rotation;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg style={`enable-background:new 0 0 35.414 35.414; transform: rotate(${rotation}deg); transition: all .4s;`}
|
|
||||||
version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
|
||||||
y="0px" viewBox="0 0 35.414 35.414" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<polygon points="27.051,17 9.905,0 8.417,1.414 24.674,17.707 8.363,34 9.914,35.414 27.051,18.414 " />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
@ -1,188 +0,0 @@
|
|||||||
<script context="module">
|
|
||||||
const TFATypes = new Map();
|
|
||||||
TFATypes.set(0, "Authenticator");
|
|
||||||
TFATypes.set(1, "Backup Codes");
|
|
||||||
TFATypes.set(2, "YubiKey");
|
|
||||||
TFATypes.set(3, "Push Notification");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Box from "./Box.svelte";
|
|
||||||
import BoxItem from "./BoxItem.svelte";
|
|
||||||
import NextIcon from "./NextIcon.svelte";
|
|
||||||
import request from "../../../helper/request.ts";
|
|
||||||
|
|
||||||
export let loading = false;
|
|
||||||
|
|
||||||
let twofactor = [];
|
|
||||||
|
|
||||||
async function deleteTFA(id) {
|
|
||||||
let res = await request(
|
|
||||||
"/api/user/twofactor/" + id,
|
|
||||||
undefined,
|
|
||||||
"DELETE",
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
loadTwoFactor();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTwoFactor() {
|
|
||||||
let res = await request(
|
|
||||||
"/api/user/twofactor",
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
twofactor = res.methods;
|
|
||||||
}
|
|
||||||
|
|
||||||
let token = [];
|
|
||||||
|
|
||||||
async function revoke(id) {
|
|
||||||
let res = await request(
|
|
||||||
"/api/user/token/" + id,
|
|
||||||
undefined,
|
|
||||||
"DELETE",
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
loadToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadToken() {
|
|
||||||
loading = true;
|
|
||||||
let res = await request(
|
|
||||||
"/api/user/token",
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
token = res.token;
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadToken();
|
|
||||||
loadTwoFactor();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn {
|
|
||||||
background-color: var(--primary);
|
|
||||||
margin: auto 0;
|
|
||||||
margin-left: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container > *:first-child {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
background-color: unset;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
color: unset;
|
|
||||||
font-size: unset;
|
|
||||||
border-bottom: 1px solid #757575;
|
|
||||||
/* Firefox */
|
|
||||||
-moz-appearance: none;
|
|
||||||
/* Safari and Chrome */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
select > option {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper::after {
|
|
||||||
content: ">";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
right: 2rem;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 1rem;
|
|
||||||
transform: rotate(90deg) scaleY(2);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<h1>Two Factor</h1>
|
|
||||||
<BoxItem name="Add new" open={false} />
|
|
||||||
{#each twofactor as t}
|
|
||||||
<BoxItem name={TFATypes.get(t.type)} value={t.name} highlight={t.isthis}>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
style="background: var(--error)"
|
|
||||||
on:click={() => deleteTFA(t.id)}>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</BoxItem>
|
|
||||||
{/each}
|
|
||||||
<!-- <BoxItem name="Name" value={name} open={false}>
|
|
||||||
<div class="input-container">
|
|
||||||
<div class="floating group">
|
|
||||||
<input type="text" autocomplete="username" bind:value={name}>
|
|
||||||
<span class="highlight"></span>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label>Name</label>
|
|
||||||
</div>
|
|
||||||
<button class="btn" on:click={saveName}>Save</button>
|
|
||||||
</div>
|
|
||||||
</BoxItem>
|
|
||||||
<BoxItem name="Gender" value={gender} open={true}>
|
|
||||||
<div class="input-container">
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<select>
|
|
||||||
<option value="1">Male</option>
|
|
||||||
<option value="2">Female</option>
|
|
||||||
<option value="3">Other</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="btn" on:click={saveName}>Save</button>
|
|
||||||
</div>
|
|
||||||
</BoxItem>
|
|
||||||
<BoxItem name="Birthday" value={birthday} />
|
|
||||||
<BoxItem name="Password" value="******" /> -->
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<h1>Anmeldungen</h1>
|
|
||||||
|
|
||||||
{#each token as t}
|
|
||||||
<BoxItem name={t.browser} value={t.ip} highlight={t.isthis}>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
style="background: var(--error)"
|
|
||||||
on:click={() => revoke(t.id)}>
|
|
||||||
Revoke
|
|
||||||
</button>
|
|
||||||
</BoxItem>
|
|
||||||
{:else}<span>No Tokens</span>{/each}
|
|
||||||
|
|
||||||
<!-- <BoxItem name="E-Mail" value={email} />
|
|
||||||
<BoxItem name="Phone" value={phone} /> -->
|
|
||||||
</Box>
|
|
54
Frontend/src/pages/user/Sidebar.svelte
Normal file
54
Frontend/src/pages/user/Sidebar.svelte
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarItem,
|
||||||
|
SidebarWrapper,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
import { CurrentPage } from "./nav";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
export let sidebarOpen = false;
|
||||||
|
export let sidebarOpenVisible = false;
|
||||||
|
|
||||||
|
$: open = !sidebarOpenVisible || sidebarOpen;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const mq = window.matchMedia("(max-width: 768px)");
|
||||||
|
const onChange = (e: MediaQueryListEvent) => {
|
||||||
|
sidebarOpenVisible = e.matches;
|
||||||
|
};
|
||||||
|
mq.addEventListener("change", onChange);
|
||||||
|
|
||||||
|
onChange({ matches: mq.matches } as MediaQueryListEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mq.removeEventListener("change", onChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar class="h-screen" style={open ? "display: block" : "display: none"}>
|
||||||
|
<SidebarWrapper class="h-full">
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarItem
|
||||||
|
label="Personal Data"
|
||||||
|
active={$CurrentPage == "personal-info"}
|
||||||
|
href="#personal-info"
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="icon">
|
||||||
|
<span class="material-icons-outlined"> account_circle </span>
|
||||||
|
</svelte:fragment>
|
||||||
|
</SidebarItem>
|
||||||
|
<SidebarItem
|
||||||
|
label="Security"
|
||||||
|
active={$CurrentPage == "security"}
|
||||||
|
href="#security"
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="icon">
|
||||||
|
<span class="material-icons-outlined"> lock </span>
|
||||||
|
</svelte:fragment>
|
||||||
|
</SidebarItem>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarWrapper>
|
||||||
|
</Sidebar>
|
@ -1,4 +1,4 @@
|
|||||||
import "../../components/theme";
|
import "../../main.css";
|
||||||
import App from "./App.svelte";
|
import App from "./App.svelte";
|
||||||
|
|
||||||
new App({
|
new App({
|
||||||
|
25
Frontend/src/pages/user/nav.ts
Normal file
25
Frontend/src/pages/user/nav.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
type Pages = "personal-info" | "security";
|
||||||
|
|
||||||
|
|
||||||
|
function getCurrentPage(): Pages | undefined {
|
||||||
|
let hash = window.location.hash;
|
||||||
|
if (hash.length > 0) {
|
||||||
|
hash = hash.substring(1);
|
||||||
|
if (hash === "personal-info" || hash === "security") {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CurrentPage = writable<Pages>(getCurrentPage() ?? "personal-info");
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", () => {
|
||||||
|
CurrentPage.set(getCurrentPage() ?? "personal-info");
|
||||||
|
});
|
||||||
|
|
||||||
|
export function navigateTo(page: Pages) {
|
||||||
|
window.location.hash = "#" + page;
|
||||||
|
}
|
||||||
|
|
36
Frontend/src/pages/user/pages/AddTwoFactor.svelte
Normal file
36
Frontend/src/pages/user/pages/AddTwoFactor.svelte
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Listgroup, ListgroupItem, Modal, Radio } from "flowbite-svelte";
|
||||||
|
import Totp from "./TwoFactorRegistration/TOTP.svelte";
|
||||||
|
import WebAuthn from "./TwoFactorRegistration/WebAuthn.svelte";
|
||||||
|
|
||||||
|
export let open: boolean;
|
||||||
|
|
||||||
|
let selectedType = undefined;
|
||||||
|
$: {
|
||||||
|
if (!open) {
|
||||||
|
selectedType = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:open size="md" autoclose={false} class="w-full">
|
||||||
|
{#if !selectedType}
|
||||||
|
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||||
|
Select type
|
||||||
|
</h3>
|
||||||
|
<Listgroup active class="w-full">
|
||||||
|
<ListgroupItem
|
||||||
|
class="gap-2 px-4 py-4"
|
||||||
|
on:click={() => (selectedType = "totp")}>TOTP</ListgroupItem
|
||||||
|
>
|
||||||
|
<ListgroupItem
|
||||||
|
class="gap-2 px-4 py-4"
|
||||||
|
on:click={() => (selectedType = "webauthn")}>WebAuthn</ListgroupItem
|
||||||
|
>
|
||||||
|
</Listgroup>
|
||||||
|
{:else if selectedType == "totp"}
|
||||||
|
<Totp on:reload />
|
||||||
|
{:else if selectedType == "webauthn"}
|
||||||
|
<WebAuthn on:reload />
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
203
Frontend/src/pages/user/pages/PersonalInfo.svelte
Normal file
203
Frontend/src/pages/user/pages/PersonalInfo.svelte
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type ContactInfo,
|
||||||
|
type Profile,
|
||||||
|
Gender,
|
||||||
|
} from "@hibas123/openauth-internalapi";
|
||||||
|
import InternalAPI from "../../../helper/api";
|
||||||
|
import Loading from "../Loading.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
Heading,
|
||||||
|
Spinner,
|
||||||
|
Helper,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
|
||||||
|
let profileInfo: Profile;
|
||||||
|
let loadedProfileInfo: Profile;
|
||||||
|
let contactInfo: ContactInfo;
|
||||||
|
|
||||||
|
let loading = true;
|
||||||
|
let error: string | undefined;
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
error = undefined;
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
profileInfo = await InternalAPI.Account.GetProfile();
|
||||||
|
loadedProfileInfo = { ...profileInfo };
|
||||||
|
contactInfo = await InternalAPI.Account.GetContactInfos();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let savingProfile = false;
|
||||||
|
|
||||||
|
async function saveProfileChanges() {
|
||||||
|
savingProfile = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((yes) => setTimeout(yes, 1000));
|
||||||
|
await InternalAPI.Account.UpdateProfile(profileInfo);
|
||||||
|
loadedProfileInfo = { ...profileInfo };
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
savingProfile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: hasProfileChanged =
|
||||||
|
JSON.stringify(profileInfo) != JSON.stringify(loadedProfileInfo);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
let genders = [
|
||||||
|
{
|
||||||
|
value: Gender.None,
|
||||||
|
name: "Not saying",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Gender.Male,
|
||||||
|
name: "Male",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Gender.Female,
|
||||||
|
name: "Female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Gender.Other,
|
||||||
|
name: "Other",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Loading {loading} {error}>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<Card size="md" class="w-full">
|
||||||
|
<Heading tag="h5">General Account Details</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="name-input" class="block mb-2">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name-input"
|
||||||
|
placeholder="Name"
|
||||||
|
bind:value={profileInfo.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||||
|
<Input
|
||||||
|
id="birthday-input"
|
||||||
|
placeholder="Birthday"
|
||||||
|
disabled
|
||||||
|
bind:value={profileInfo.birthday}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label class="block mb-2"
|
||||||
|
>Gender
|
||||||
|
<Select items={genders} bind:value={profileInfo.gender} />
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={!hasProfileChanged || savingProfile}
|
||||||
|
on:click={saveProfileChanges}
|
||||||
|
>
|
||||||
|
{#if savingProfile}
|
||||||
|
<Spinner class="mr-3" size="4" color="white" /> Saving...
|
||||||
|
{:else}
|
||||||
|
Save
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="md" class="w-full">
|
||||||
|
<Heading tag="h5">Contact Details (WIP)</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
|
||||||
|
<Heading tag="h6" color="gray">Mails</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
|
||||||
|
{#each contactInfo.mail as mail}
|
||||||
|
<div class="mb-6">
|
||||||
|
<!-- <Label for="mail-input" class="block mb-2">Mail</Label> -->
|
||||||
|
<Input
|
||||||
|
id="mail-input"
|
||||||
|
placeholder="Mail"
|
||||||
|
bind:value={mail.mail}
|
||||||
|
color={mail.verified ? "green" : "base"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
{#if mail.verified}
|
||||||
|
<Helper class="mt-2" color="green"
|
||||||
|
><span class="font-medium">Well done!</span> E-Mail is verified.</Helper
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<Helper class="mt-2" color="gray"
|
||||||
|
><span class="font-medium">Oh no!</span> E-Mail needs verification.</Helper
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<Heading tag="h6" color="gray">Phones</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
|
||||||
|
{#each contactInfo.phone as phone}
|
||||||
|
<div class="mb-6">
|
||||||
|
<!-- <Label for="phone-input" class="block mb-2">Phone</Label> -->
|
||||||
|
<Input
|
||||||
|
id="phone-input"
|
||||||
|
placeholder="Phone"
|
||||||
|
bind:value={phone.phone}
|
||||||
|
color={phone.verified ? "green" : "base"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
{#if phone.verified}
|
||||||
|
<Helper class="mt-2" color="green"
|
||||||
|
><span class="font-medium">Well done!</span> Phone is verified.</Helper
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<Helper class="mt-2" color="gray"
|
||||||
|
><span class="font-medium">Oh no!</span> Phone needs verification.</Helper
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<!-- <div class="mb-6">
|
||||||
|
<Label for="name-input" class="block mb-2">Name</Label>
|
||||||
|
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||||
|
<Input
|
||||||
|
id="birthday-input"
|
||||||
|
placeholder="Birthday"
|
||||||
|
disabled
|
||||||
|
bind:value={profileInfo.birthday}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label class="block mb-2"
|
||||||
|
>Gender
|
||||||
|
<Select items={genders} bind:value={profileInfo.gender} />
|
||||||
|
</Label>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <Button>Save</Button> -->
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Loading>
|
217
Frontend/src/pages/user/pages/Security.svelte
Normal file
217
Frontend/src/pages/user/pages/Security.svelte
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Session, TFAOption, TFAType } 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,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableHeadCell,
|
||||||
|
TableBody,
|
||||||
|
TableBodyRow,
|
||||||
|
TableBodyCell,
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
Alert,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
import AddTwoFactor from "./AddTwoFactor.svelte";
|
||||||
|
|
||||||
|
let tokens: Session[];
|
||||||
|
let twofactors: TFAOption[];
|
||||||
|
let error: string | undefined;
|
||||||
|
let loading = true;
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
tokens = await InternalAPI.Security.GetSessions();
|
||||||
|
twofactors = await InternalAPI.TwoFactor.GetOptions();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload() {
|
||||||
|
try {
|
||||||
|
tokens = await InternalAPI.Security.GetSessions();
|
||||||
|
twofactors = await InternalAPI.TwoFactor.GetOptions();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function revokeToken(id: string) {
|
||||||
|
try {
|
||||||
|
await InternalAPI.Security.RevokeSession(id);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeToName = {
|
||||||
|
[TFAType.TOTP]: "TOTP",
|
||||||
|
[TFAType.WEBAUTHN]: "Security Key (WebAuthn)",
|
||||||
|
[TFAType.BACKUP_CODE]: "Backup-Code",
|
||||||
|
[TFAType.APP_ALLOW]: "App-Auth",
|
||||||
|
};
|
||||||
|
|
||||||
|
let addTwoFactorOpen = false;
|
||||||
|
|
||||||
|
function openAddTwoFactor() {
|
||||||
|
addTwoFactorOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTwoFactor(id: string) {
|
||||||
|
try {
|
||||||
|
await InternalAPI.TwoFactor.Delete(id);
|
||||||
|
await reload();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_pw = "";
|
||||||
|
let new_pw = "";
|
||||||
|
let new_pw_repeat = "";
|
||||||
|
|
||||||
|
let change_password_success = false;
|
||||||
|
let change_password_error: string | undefined;
|
||||||
|
|
||||||
|
function changePassword() {
|
||||||
|
change_password_success = false;
|
||||||
|
change_password_error = undefined;
|
||||||
|
if (new_pw !== new_pw_repeat) {
|
||||||
|
change_password_error = "Passwords do not match";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalAPI.Security.ChangePassword(old_pw, new_pw)
|
||||||
|
.then(() => {
|
||||||
|
change_password_error = undefined;
|
||||||
|
old_pw = "";
|
||||||
|
new_pw = "";
|
||||||
|
new_pw_repeat = "";
|
||||||
|
change_password_success = true;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
change_password_error = e.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Loading {loading} {error}>
|
||||||
|
<Card size="xl">
|
||||||
|
<Heading tag="h5">Active Sessions</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableHeadCell>Browser</TableHeadCell>
|
||||||
|
<TableHeadCell class="w-full">IP</TableHeadCell>
|
||||||
|
<TableHeadCell class="material-icons-outlined w-20"
|
||||||
|
>delete</TableHeadCell
|
||||||
|
>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{#each tokens as token}
|
||||||
|
<TableBodyRow
|
||||||
|
class="bg-yellow-50"
|
||||||
|
color={token.isthis ? "custom" : "default"}
|
||||||
|
>
|
||||||
|
<TableBodyCell>{token.browser}</TableBodyCell>
|
||||||
|
<TableBodyCell>{token.ip}</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<a
|
||||||
|
class="font-medium text-red-600 hover:underline dark:text-blue-500"
|
||||||
|
on:click={() => revokeToken(token.id)}
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</a>
|
||||||
|
</TableBodyCell>
|
||||||
|
</TableBodyRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="xl" class="mt-4">
|
||||||
|
<Heading tag="h5">Change Password</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
|
||||||
|
{#if change_password_success}
|
||||||
|
<Alert color="green">Password changed successfully.</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if change_password_error}
|
||||||
|
<Alert color="red">{change_password_error}</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="oldPassword">Old Password</Label>
|
||||||
|
<Input type="password" id="oldPassword" bind:value={old_pw} />
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="newPassword">New Password</Label>
|
||||||
|
<Input type="password" id="newPassword" bind:value={new_pw} />
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="newPasswordRepeat">Repeat New Password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
id="newPasswordRepeat"
|
||||||
|
bind:value={new_pw_repeat}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button class="mt-4" on:click={changePassword}>Change Password</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="xl" class="mt-4">
|
||||||
|
<Heading tag="h5">Two Factor Auth</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
|
||||||
|
<Accordion>
|
||||||
|
{#each twofactors as tfa}
|
||||||
|
<AccordionItem>
|
||||||
|
<span slot="header">{tfa.name ?? typeToName[tfa.tfatype]}</span>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
class="mt-4"
|
||||||
|
on:click={() => deleteTwoFactor(tfa.id)}>Delete</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
{/each}
|
||||||
|
</Accordion>
|
||||||
|
<Button class="mt-4" on:click={openAddTwoFactor}>Add Option</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<AddTwoFactor on:reload={reload} bind:open={addTwoFactorOpen} />
|
||||||
|
|
||||||
|
<!-- <Card size="xl" class="mt-4">
|
||||||
|
<Heading tag="h5">Delete Account</Heading>
|
||||||
|
<hr class="mb-6" />
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="password">Password</Label>
|
||||||
|
<Input type="password" id="password" />
|
||||||
|
</div>
|
||||||
|
<Button class="mt-4">Delete Account</Button>
|
||||||
|
</Card> -->
|
||||||
|
</Loading>
|
102
Frontend/src/pages/user/pages/TwoFactorRegistration/TOTP.svelte
Normal file
102
Frontend/src/pages/user/pages/TwoFactorRegistration/TOTP.svelte
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Alert, Button, Input, Label, Spinner } from "flowbite-svelte";
|
||||||
|
import InternalAPI from "../../../../helper/api";
|
||||||
|
|
||||||
|
import type { TFANewTOTP } from "@hibas123/openauth-internalapi";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let stage = "get-name";
|
||||||
|
let name: string = "";
|
||||||
|
let code: string = "";
|
||||||
|
let totp: TFANewTOTP;
|
||||||
|
|
||||||
|
let creatingTOTP = false;
|
||||||
|
let verifingTOTP = false;
|
||||||
|
|
||||||
|
async function createTOTP() {
|
||||||
|
creatingTOTP = true;
|
||||||
|
try {
|
||||||
|
totp = await InternalAPI.TwoFactor.AddTOTP(name);
|
||||||
|
stage = "verify";
|
||||||
|
} catch (err) {
|
||||||
|
} finally {
|
||||||
|
creatingTOTP = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let verifyError = undefined;
|
||||||
|
async function verifyTOTP() {
|
||||||
|
verifingTOTP = true;
|
||||||
|
verifyError = undefined;
|
||||||
|
try {
|
||||||
|
await InternalAPI.TwoFactor.VerifyTOTP(totp.id, code);
|
||||||
|
stage = "done";
|
||||||
|
dispatch("reload");
|
||||||
|
} catch (err) {
|
||||||
|
verifyError = err.message;
|
||||||
|
code = "";
|
||||||
|
} finally {
|
||||||
|
verifingTOTP = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if stage == "get-name"}
|
||||||
|
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||||
|
Select a name
|
||||||
|
</h3>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="name-input" class="block mb-2">Name</Label>
|
||||||
|
<Input id="name-input" placeholder="Name" bind:value={name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button disabled={creatingTOTP} on:click={createTOTP}>
|
||||||
|
{#if creatingTOTP}
|
||||||
|
<Spinner class="mr-3" size="4" color="white" /> Creating...
|
||||||
|
{:else}
|
||||||
|
Create
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{:else if stage == "verify"}
|
||||||
|
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||||
|
Save secret and verify
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<img class="w-64" src={totp.qr} alt="Secret {totp.secret}" />
|
||||||
|
<div>Manually:</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{totp.secret}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="code-input" class="block mb-2">Code</Label>
|
||||||
|
<Input id="code-input" placeholder="Code" bind:value={code} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if verifyError}
|
||||||
|
<Alert color="red">
|
||||||
|
<h2 class="text-lg font-bold">Error</h2>
|
||||||
|
<p class="mt-2">{verifyError}</p>
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Button disabled={verifingTOTP} on:click={verifyTOTP}>
|
||||||
|
{#if verifingTOTP}
|
||||||
|
<Spinner class="mr-3" size="4" color="white" /> Verify...
|
||||||
|
{:else}
|
||||||
|
Verify
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{:else if stage == "done"}
|
||||||
|
<Alert color="green">
|
||||||
|
<h2 class="text-lg font-bold">Success</h2>
|
||||||
|
<p class="mt-2">Your TOTP has been created.</p>
|
||||||
|
</Alert>
|
||||||
|
{:else}
|
||||||
|
<Alert color="red">
|
||||||
|
<h2 class="text-lg font-bold">Error</h2>
|
||||||
|
<p class="mt-2">An unknown error occured.</p>
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Alert, Button, Input, Label, Spinner } from "flowbite-svelte";
|
||||||
|
import { startRegistration } from "@simplewebauthn/browser";
|
||||||
|
import InternalAPI from "../../../../helper/api";
|
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let stage = "get-name";
|
||||||
|
let name: string = "";
|
||||||
|
|
||||||
|
let creating = false;
|
||||||
|
let error = undefined;
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
creating = true;
|
||||||
|
error = undefined;
|
||||||
|
try {
|
||||||
|
let challenge_data = await InternalAPI.TwoFactor.AddWebauthn(name);
|
||||||
|
let challenge = JSON.parse(challenge_data.challenge);
|
||||||
|
stage = "verify";
|
||||||
|
|
||||||
|
creating = false;
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
console.log(challenge);
|
||||||
|
let response = await startRegistration(challenge);
|
||||||
|
|
||||||
|
await InternalAPI.TwoFactor.VerifyWebAuthn(
|
||||||
|
challenge_data.id,
|
||||||
|
JSON.stringify(response)
|
||||||
|
);
|
||||||
|
|
||||||
|
stage = "done";
|
||||||
|
dispatch("reload");
|
||||||
|
} catch (err) {
|
||||||
|
error = err.message;
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
creating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<Alert color="red">
|
||||||
|
<h2 class="text-lg font-bold">Error</h2>
|
||||||
|
<p class="mt-2">An unknown error occured.</p>
|
||||||
|
</Alert>
|
||||||
|
{:else if stage == "get-name"}
|
||||||
|
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||||
|
Select a name
|
||||||
|
</h3>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="name-input" class="block mb-2">Name</Label>
|
||||||
|
<Input id="name-input" placeholder="Name" bind:value={name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button disabled={creating} on:click={register}>
|
||||||
|
{#if creating}
|
||||||
|
<Spinner class="mr-3" size="4" color="white" /> Creating...
|
||||||
|
{:else}
|
||||||
|
Create
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{:else if stage == "verify"}
|
||||||
|
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||||
|
Select device to add
|
||||||
|
</h3>
|
||||||
|
<!-- <div class="flex flex-col justify-center items-center">
|
||||||
|
<img class="w-64" src={totp.qr} alt="Secret {totp.secret}" />
|
||||||
|
<div>Manually:</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{totp.secret}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<Label for="code-input" class="block mb-2">Code</Label>
|
||||||
|
<Input id="code-input" placeholder="Code" bind:value={code} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if verifyError}
|
||||||
|
<Alert color="red">
|
||||||
|
<h2 class="text-lg font-bold">Error</h2>
|
||||||
|
<p class="mt-2">{verifyError}</p>
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Button disabled={verifing} on:click={verifyTOTP}>
|
||||||
|
{#if verifing}
|
||||||
|
<Spinner class="mr-3" size="4" color="white" /> Verify...
|
||||||
|
{:else}
|
||||||
|
Verify
|
||||||
|
{/if}
|
||||||
|
</Button> -->
|
||||||
|
{:else if stage == "done"}
|
||||||
|
<Alert color="green">
|
||||||
|
<h2 class="text-lg font-bold">Success</h2>
|
||||||
|
<p class="mt-2">Your WebAuthn device has been registered.</p>
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
BIN
Frontend/static/material-icons-outlined-v109-latin-regular.woff
Normal file
BIN
Frontend/static/material-icons-outlined-v109-latin-regular.woff
Normal file
Binary file not shown.
BIN
Frontend/static/material-icons-outlined-v109-latin-regular.woff2
Normal file
BIN
Frontend/static/material-icons-outlined-v109-latin-regular.woff2
Normal file
Binary file not shown.
BIN
Frontend/static/material-icons-v140-latin-regular.woff
Normal file
BIN
Frontend/static/material-icons-v140-latin-regular.woff
Normal file
Binary file not shown.
BIN
Frontend/static/material-icons-v140-latin-regular.woff2
Normal file
BIN
Frontend/static/material-icons-v140-latin-regular.woff2
Normal file
Binary file not shown.
15
Frontend/tailwind.config.js
Normal file
15
Frontend/tailwind.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{html,js,svelte,ts}",
|
||||||
|
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
|
||||||
|
"../node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
|
||||||
|
],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [require("flowbite/plugin")],
|
||||||
|
darkMode: "class",
|
||||||
|
};
|
@ -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
16
InternalAPI/account.jrpc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
type UserRegisterInfo {
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
mail: string;
|
||||||
|
password: string;
|
||||||
|
salt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
service AccountService {
|
||||||
|
Register(regcode: string, info: UserRegisterInfo): void;
|
||||||
|
GetProfile(): Profile;
|
||||||
|
UpdateProfile(info: Profile): void;
|
||||||
|
GetContactInfos(): ContactInfo;
|
||||||
|
}
|
||||||
|
|
5
InternalAPI/api.jrpc
Normal file
5
InternalAPI/api.jrpc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import "./types";
|
||||||
|
import "./twofactor";
|
||||||
|
import "./login";
|
||||||
|
import "./account";
|
||||||
|
import "./security";
|
21
InternalAPI/login.jrpc
Normal file
21
InternalAPI/login.jrpc
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import "./twofactor";
|
||||||
|
|
||||||
|
type LoginState {
|
||||||
|
success: boolean;
|
||||||
|
username?: string;
|
||||||
|
password?: boolean;
|
||||||
|
passwordSalt?: string;
|
||||||
|
requireTwoFactor?: TFAOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
service LoginService {
|
||||||
|
GetState(): LoginState;
|
||||||
|
Start(username: string): LoginState;
|
||||||
|
UsePassword(password_hash: string, date: int): LoginState;
|
||||||
|
|
||||||
|
UseTOTP(id: string, code: string): LoginState;
|
||||||
|
UseBackupCode(id: string, code:string): LoginState;
|
||||||
|
GetWebAuthnChallenge(id: string): string;
|
||||||
|
UseWebAuthn(id: string, response: string): LoginState;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user