Start implementing a new user page for account and security settings

This commit is contained in:
Fabian Stamm 2023-04-09 18:20:43 +02:00
parent 1e2bb83447
commit 922ed1e813
46 changed files with 2307 additions and 443 deletions

View File

@ -45,6 +45,7 @@
"@hibas123/config": "^1.1.2",
"@hibas123/nodelogging": "^3.1.3",
"@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/openauth-internalapi": "workspace:^",
"@hibas123/openauth-views-v1": "workspace:^",
"@hibas123/safe_mongo": "^1.7.1",
"body-parser": "^1.20.2",

View File

@ -7,6 +7,7 @@ import ClientRouter from "./client";
import * as cors from "cors";
import OAuthRoute from "./oauth";
import config from "../config";
import JRPCEndpoint from "./jrpc";
const ApiRouter: express.IRouter = express.Router();
ApiRouter.use("/admin", AdminRoute);
@ -17,6 +18,26 @@ ApiRouter.use("/oauth", OAuthRoute);
ApiRouter.use("/client", ClientRouter);
/**
* @api {post} /jrpc
* @apiName InternalJRPCEndpoint
*
* @apiGroup user
* @apiPermission none
*
* @apiErrorExample {Object} Error-Response:
{
error: [
{
message: "Some Error",
field: "username"
}
],
status: 400
}
*/
ApiRouter.post("/jrpc", JRPCEndpoint);
// Legacy reasons (deprecated)
ApiRouter.use("/", ClientRouter);

View File

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

View File

@ -0,0 +1,45 @@
import { Request, Response } from "express";
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import { IUser } from "../../models/user";
import { Server } from "@hibas123/openauth-internalapi";
import AccountService from "./account_service";
import SecurityService from "./security_service";
import { ILoginToken } from "../../models/login_token";
export interface SessionContext {
user: IUser,
request: Request,
isAdmin: boolean,
special: boolean,
token: {
login: ILoginToken,
special?: ILoginToken,
}
}
const provider = new Server.ServiceProvider<SessionContext>();
provider.addService(new AccountService());
provider.addService(new SecurityService());
const JRPCEndpoint = Stacker(
GetUserMiddleware(true, true),
async (req: Request, res: Response) => {
const session = provider.getSession((data) => {
res.json(data);
}, {
user: req.user,
request: req,
isAdmin: req.isAdmin,
special: req.special,
token: {
login: req.token.login,
special: req.token.special,
}
});
session.onMessage(req.body);
}
);
export default JRPCEndpoint;

View File

@ -0,0 +1,71 @@
import { Server, Token, TwoFactor, UserRegisterInfo } from "@hibas123/openauth-internalapi";
import type { SessionContext } from "./index";
import LoginToken, { CheckToken } from "../../models/login_token";
import TwoFactorModel from "../../models/twofactor";
import moment = require("moment");
export default class SecurityService extends Server.SecurityService<SessionContext> {
async GetTokens(ctx: SessionContext): Promise<Token[]> {
if (!ctx.user) throw new Error("Not logged in");
let raw_token = await LoginToken.find({
user: ctx.user._id,
valid: true,
});
let token = await Promise.all(
raw_token
.map<Promise<Token>>(async (token) => {
await CheckToken(token);
return {
id: token._id.toString(),
special: token.special,
ip: token.ip,
browser: token.browser,
isthis: token._id.equals(
token.special ? ctx.token.special._id : ctx.token.login._id
),
};
})
.filter((t) => t !== undefined)
);
return token
}
async RevokeToken(id: string, ctx: SessionContext): Promise<void> {
if (!ctx.user) throw new Error("Not logged in");
let token = await LoginToken.findById(id);
if (!token || !token.user.equals(ctx.user._id))
throw new Error("Invalid ID");
token.valid = false;
await LoginToken.save(token);
}
async GetTwofactorOptions(ctx: SessionContext): Promise<TwoFactor[]> {
if (!ctx.user) throw new Error("Not logged in");
let twofactor = await TwoFactorModel.find({ user: ctx.user._id, valid: true });
let expired = twofactor.filter((e) =>
e.expires ? moment().isAfter(moment(e.expires)) : false
);
await Promise.all(
expired.map((e) => {
e.valid = false;
return TwoFactorModel.save(e);
})
);
twofactor = twofactor.filter((e) => e.valid);
let tfa = twofactor.map<TwoFactor>((e) => {
return {
id: e._id.toString(),
name: e.name,
tfatype: e.type as number,
expires: e.expires?.valueOf()
};
});
return tfa;
}
}

View File

@ -18,6 +18,8 @@ export default Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) =
email: mail.mail,
username: req.user.username,
displayName: req.user.name,
"display-name": req.user.name,
displayNameClaim: req.user.name,
name: req.user.name,
});
})

View File

@ -33,7 +33,7 @@ BackupCodeRoute.post(
console.log(codes);
let twofactor = TwoFactor.new(<IBackupCode>{
user: req.user._id,
type: TwoFATypes.OTC,
type: TwoFATypes.TOTP,
valid: true,
data: codes,
name: "",
@ -60,7 +60,7 @@ BackupCodeRoute.put(
!twofactor ||
!twofactor.valid ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC
twofactor.type !== TwoFATypes.TOTP
) {
throw new RequestError(
"Invalid Method!",

View File

@ -28,7 +28,7 @@ OTCRoute.post(
});
let twofactor = TwoFactor.new(<IOTC>{
user: req.user._id,
type: TwoFATypes.OTC,
type: TwoFATypes.TOTP,
valid: false,
data: secret.base32,
});
@ -49,7 +49,7 @@ OTCRoute.post(
if (
!twofactor ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC ||
twofactor.type !== TwoFATypes.TOTP ||
!twofactor.data ||
twofactor.valid
) {
@ -96,7 +96,7 @@ OTCRoute.put(
!twofactor ||
!twofactor.valid ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC
twofactor.type !== TwoFATypes.TOTP
) {
throw new RequestError(
"Invalid Method!",

View File

@ -27,7 +27,7 @@ U2FRoute.post(
let twofactor = TwoFactor.new(<IYubiKey>{
user: req.user._id,
type: TwoFATypes.U2F,
type: TwoFATypes.WEBAUTHN,
valid: false,
data: {
registration: registrationRequest,
@ -49,7 +49,7 @@ U2FRoute.post(
if (
!twofactor ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.U2F ||
twofactor.type !== TwoFATypes.WEBAUTHN ||
!twofactor.data.registration ||
twofactor.valid
) {
@ -95,7 +95,7 @@ U2FRoute.get(
let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({
user: req.user._id,
type: TwoFATypes.U2F,
type: TwoFATypes.WEBAUTHN,
valid: true,
});
@ -142,7 +142,7 @@ U2FRoute.put(
let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({
user: req.user._id,
type: TwoFATypes.U2F,
type: TwoFATypes.WEBAUTHN,
valid: true,
});

View File

@ -3,16 +3,16 @@ import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "bson";
export enum TFATypes {
OTC,
TOTP,
BACKUP_CODE,
U2F,
WEBAUTHN,
APP_ALLOW,
}
export const TFANames = new Map<TFATypes, string>();
TFANames.set(TFATypes.OTC, "Authenticator");
TFANames.set(TFATypes.TOTP, "Authenticator");
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
TFANames.set(TFATypes.U2F, "Security Key (U2F)");
TFANames.set(TFATypes.WEBAUTHN, "Security Key (WebAuthn)");
TFANames.set(TFATypes.APP_ALLOW, "App Push");
export interface ITwoFactor extends ModelDataBase {
@ -53,7 +53,7 @@ const TwoFactor = DB.addModel<ITwoFactor>({
name: "twofactor",
versions: [
{
migration: (e) => {},
migration: (e) => { },
schema: {
user: { type: ObjectID },
valid: { type: Boolean },

View File

@ -96,6 +96,7 @@ export default async function TestData() {
if (!t) {
t = TwoFactor.new({
user: u._id,
name: "Test OTP",
type: 0,
valid: true,
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",

View File

@ -49,7 +49,13 @@ ViewRouter.use(
ViewRouter.use(
"/user",
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) => {

View File

@ -2,12 +2,18 @@
"name": "@hibas123/openauth-views-v2",
"main": "index.js",
"devDependencies": {
"@popperjs/core": "^2.11.7",
"@rollup/plugin-html": "^1.0.2",
"@rollup/plugin-image": "^3.0.2",
"@rollup/plugin-node-resolve": "^15.0.2",
"@tsconfig/svelte": "^4.0.1",
"@types/cleave.js": "^1.4.7",
"autoprefixer": "^10.4.14",
"classnames": "^2.3.2",
"cssnano": "^6.0.0",
"esbuild": "^0.17.15",
"flowbite": "^1.6.4",
"flowbite-svelte": "^0.34.7",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"postcss-url": "^10.1.3",
@ -20,6 +26,7 @@
"rollup-plugin-visualizer": "^5.9.0",
"svelte": "^3.58.0",
"svelte-preprocess": "^5.0.3",
"tailwindcss": "^3.3.1",
"typescript": "^5.0.3"
},
"scripts": {
@ -28,8 +35,10 @@
"dev": "rollup -c rollup.config.mjs -w"
},
"dependencies": {
"@hibas123/openauth-internalapi": "workspace:^",
"@hibas123/theme": "^2.0.6",
"@hibas123/utils": "^2.2.18",
"@rollup/plugin-commonjs": "^24.0.1",
"cleave.js": "^1.6.0",
"what-the-pack": "^2.0.3"
}

View File

@ -1,3 +1,7 @@
module.exports = {
plugins: [],
plugins: {
tailwindcss: {},
autoprefixer: {},
cssnano: {},
},
};

View File

@ -8,6 +8,7 @@ import { visualizer } from "rollup-plugin-visualizer";
import postcss from "rollup-plugin-postcss";
import livereload from "rollup-plugin-livereload";
import sveltePreprocess from "svelte-preprocess";
import commonjs from "@rollup/plugin-commonjs";
const VIEWS = ["home", "login", "popup", "user"];
@ -66,30 +67,20 @@ const htmlTemplate = ({ attributes, meta, files, publicPath, title }) => {
export default VIEWS.map((view) => ({
input: `src/pages/${view}/main.ts`,
output: [
dev
? {
file: `build/${view}/bundle.js`,
format: "iife",
sourcemap: true,
name: view,
}
: {
file: `build/${view}/bundle.min.js`,
format: "iife",
name: view,
plugins: [
esbuild({
minify: true,
}),
],
},
{
file: `build/${view}/bundle.min.js`,
format: "es",
sourcemap: true,
name: view,
},
],
plugins: [
svelte({
emitCss: true,
preprocess: sveltePreprocess({}),
}),
esbuild({ sourceMap: dev }),
commonjs(),
esbuild({ sourceMap: dev, minify: true }),
html({
title: view,
attributes: {
@ -105,8 +96,8 @@ export default VIEWS.map((view) => ({
}),
resolve({
browser: true,
dedupe: ["svelte"],
exportConditions: ["svelte"],
extensions: [".svelte"],
}),
image(),
sizes(),

View File

@ -0,0 +1,23 @@
import { Client } from "@hibas123/openauth-internalapi";
import request, { RequestError } from "./request";
const provider = new Client.ServiceProvider((data) => {
request("/api/jrpc", {}, "POST", data, true, true).then(result => {
provider.onPacket(result);
}).catch(err => {
if (err instanceof RequestError) {
let data = err.response;
if (data.error && Array.isArray(data.error)) {
data.error = data.error[0];
}
provider.onPacket(data);
}
});
});
const InternalAPI = {
Account: new Client.AccountService(provider),
Security: new Client.SecurityService(provider),
}
export default InternalAPI;

View File

@ -2,6 +2,15 @@ import { getCookie } from "./cookie";
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(
endpoint: string,
parameters: { [key: string]: string } = {},
@ -46,7 +55,7 @@ export default async function request(
);
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;
});

68
Frontend/src/main.css Normal file
View 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;
}

View File

@ -1,18 +1,3 @@
<style>
.main {
padding: 2rem;
}
li {
list-style: none;
padding: 1rem;
}
li > a {
text-decoration: none;
}
</style>
<div class="main">
<h1>Home Page</h1>
@ -42,3 +27,18 @@
</li>
</ul>
</div>
<style>
.main {
padding: 2rem;
}
li {
list-style: none;
padding: 1rem;
}
li > a {
text-decoration: none;
}
</style>

View File

@ -1,207 +1,25 @@
<script>
import AccountPage from "./Pages/Account.svelte";
import SecurityPage from "./Pages/Security.svelte";
import { slide, fade } from "svelte/transition";
const pages = [
{
id: "account",
title: "Account",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgdmlld0JveD0iMCAwIDUwMCA1MDAiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT4wMSBAZnVsbHdpZHRoPC90aXRsZT48ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz48ZyBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj48ZyBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDU3LjUsMjUwIEM0NTcuNSwxMzUuOTUzMTk5IDM2NS4wNDY4MDEsNDMuNSAyNTEsNDMuNSBDMTM2Ljk1MzE5OSw0My41IDQ0LjUsMTM1Ljk1MzE5OSA0NC41LDI1MCBDNDQuNSwzNjQuMDQ2ODAxIDEzNi45NTMxOTksNDU2LjUgMjUxLDQ1Ni41IEMzNjUuMDQ2ODAxLDQ1Ni41IDQ1Ny41LDM2NC4wNDY4MDEgNDU3LjUsMjUwIFogTTU3LjUsMjUwIEM1Ny41LDE0My4xMzI5MDEgMTQ0LjEzMjkwMSw1Ni41IDI1MSw1Ni41IEMzNTcuODY3MDk5LDU2LjUgNDQ0LjUsMTQzLjEzMjkwMSA0NDQuNSwyNTAgQzQ0NC41LDM1Ni44NjcwOTkgMzU3Ljg2NzA5OSw0NDMuNSAyNTEsNDQzLjUgQzE0NC4xMzI5MDEsNDQzLjUgNTcuNSwzNTYuODY3MDk5IDU3LjUsMjUwIFoiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD48cGF0aCBkPSJNMjUxLjUsMjUyLjkzMzk2MiBDMTk2Ljg1NDE5LDI1Mi45MzM5NjIgMTUyLjUsMjk2LjM2MDgwOSAxNTIuNSwzNTAgQzE1Mi41LDM1My41ODk4NTEgMTU1LjQxMDE0OSwzNTYuNSAxNTksMzU2LjUgTDM0NCwzNTYuNSBDMzQ3LjU4OTg1MSwzNTYuNSAzNTAuNSwzNTMuNTg5ODUxIDM1MC41LDM1MCBDMzUwLjUsMjk2LjM2MDgwOSAzMDYuMTQ1ODEsMjUyLjkzMzk2MiAyNTEuNSwyNTIuOTMzOTYyIFogTTE2NS41LDM0My41MDAwMDEgQzE2NS41LDMwMy42MDI3MDggMjAzLjk3MzEzMSwyNjUuOTMzOTYyIDI1MS41LDI2NS45MzM5NjIgQzI5OS4wMjY4NjksMjY1LjkzMzk2MiAzMzcuNSwzMDMuNjAyNzA4IDMzNy40OTk5OTcsMzQzLjUwMDAwMSBMMTY1LjUsMzQzLjUwMDAwMSBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PHBhdGggZD0iTTMwNC4yNSwxOTMuMzk2MjI2IEMzMDQuMjUsMTY1LjgwODIwMiAyODEuNDY1NDI0LDE0My41IDI1My40MjcwODMsMTQzLjUgQzIyNS4zODg3NDIsMTQzLjUgMjAyLjYwNDE2NywxNjUuODA4MjAyIDIwMi42MDQxNjcsMTkzLjM5NjIyNiBDMjAyLjYwNDE2NywyMjAuOTg0MjUgMjI1LjM4ODc0MiwyNDMuMjkyNDUzIDI1My40MjcwODMsMjQzLjI5MjQ1MyBDMjgxLjQ2NTQyNCwyNDMuMjkyNDUzIDMwNC4yNSwyMjAuOTg0MjUgMzA0LjI1LDE5My4zOTYyMjYgWiBNMjE1LjYwNDE2NywxOTMuMzk2MjI2IEMyMTUuNjA0MTY3LDE3My4wNTAxMDIgMjMyLjUwNzY4MywxNTYuNSAyNTMuNDI3MDgzLDE1Ni41IEMyNzQuMzQ2NDg0LDE1Ni41IDI5MS4yNSwxNzMuMDUwMTAyIDI5MS4yNSwxOTMuMzk2MjI2IEMyOTEuMjUsMjEzLjc0MjM1MSAyNzQuMzQ2NDg0LDIzMC4yOTI0NTMgMjUzLjQyNzA4MywyMzAuMjkyNDUzIEMyMzIuNTA3NjgzLDIzMC4yOTI0NTMgMjE1LjYwNDE2NywyMTMuNzQyMzUxIDIxNS42MDQxNjcsMTkzLjM5NjIyNiBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PC9nPjwvZz48L3N2Zz4=",
component: AccountPage,
},
{
id: "security",
title: "Security",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyIDUxMjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik00NDUuOCwzNy4zYy0xLjItMC43LTIuNy0wLjgtMy45LTAuMWMtNC4zLDIuMi0xMS42LDMuNC0yMS45LDMuNGMtMzMuNiwwLTkwLjgtMTIuNC0xMjguNy0yMC41Yy0xNC0zLTI2LjEtNS42LTM0LjEtNyAgIGMtMC40LTAuMS0wLjktMC4xLTEuNCwwYy03LjIsMS4yLTE4LjUsMy42LTMxLjcsNi4zQzE4NC4zLDI3LjYsMTI0LjIsNDAsOTAuNiw0MGMtMTEuNiwwLTE3LTEuNS0xOS41LTIuOCAgIGMtMS4yLTAuNi0yLjctMC42LTMuOSwwLjFzLTEuOSwyLTEuOSwzLjRjMCw3My4xLDMuOCwxNjguNCwzMy45LDI1Ny43YzE0LjMsNDIuOCwzMy41LDgwLjcsNTcuMSwxMTIuNyAgIGMyNi42LDM2LDU5LjcsNjUuNyw5OC4zLDg4LjNjMC42LDAuNCwxLjMsMC41LDIsMC41czEuNC0wLjIsMi0wLjVjMzguNi0yMi42LDcxLjYtNTIuMyw5OC4yLTg4LjNjMjMuNi0zMiw0Mi45LTY5LjksNTcuMS0xMTIuNyAgIGMyOS41LTg3LjYsMzMuNy0xNzkuNCwzMy45LTI1Ny43QzQ0Ny43LDM5LjQsNDQ3LDM4LjEsNDQ1LjgsMzcuM3ogTTQwNi4zLDI5NS45Yy0yOS4zLDg4LjEtNzkuNywxNTMuOC0xNDkuOCwxOTUuNCAgIEMxODYuNCw0NDkuNywxMzYsMzg0LDEwNi43LDI5NS45Qzc3LjgsMjEwLDczLjQsMTE4LjEsNzMuMiw0Ni40YzQuNSwxLjEsMTAuMiwxLjYsMTcuMywxLjZjMCwwLDAsMCwwLDAgICBjMzQuNSwwLDk1LjEtMTIuNiwxMzUuMi0yMC45YzEyLjctMi42LDIzLjYtNC45LDMwLjctNi4xYzcuOCwxLjQsMTkuNSwzLjksMzMuMSw2LjhDMzMwLDM2LjYsMzg1LjUsNDguNiw0MjAsNDguNiAgIGM4LjEsMCwxNC43LTAuNywxOS43LTJDNDM5LjMsMTIyLjksNDM0LjcsMjExLjUsNDA2LjMsMjk1Ljl6Ij48L3BhdGg+PHBhdGggZD0iTTI1Ni41LDIxNy44YzQ1LDAsODEuNi0zNi42LDgxLjYtODEuNmMwLTQ1LTM2LjYtODEuNi04MS42LTgxLjZjLTQ1LDAtODEuNiwzNi42LTgxLjYsODEuNiAgIEMxNzQuOSwxODEuMiwyMTEuNSwyMTcuOCwyNTYuNSwyMTcuOHogTTI1Ni41LDYyLjZjNDAuNiwwLDczLjYsMzMsNzMuNiw3My42YzAsNDAuNi0zMyw3My42LTczLjYsNzMuNmMtNDAuNiwwLTczLjYtMzMtNzMuNi03My42ICAgQzE4Mi45LDk1LjYsMjE1LjksNjIuNiwyNTYuNSw2Mi42eiI+PC9wYXRoPjxwYXRoIGQ9Ik0zMDkuMiwyMjguOUgyMDMuOGMtMjYuNSwwLTQ4LDIxLjUtNDgsNDh2NzljMCwyLjIsMS44LDQsNCw0aDE5My40YzIuMiwwLDQtMS44LDQtNHYtNzkgICBDMzU3LjIsMjUwLjQsMzM1LjYsMjI4LjksMzA5LjIsMjI4Ljl6IE0zNDkuMiwzNTEuOUgxNjMuOHYtNzVjMC0yMiwxNy45LTQwLDQwLTQwaDEwNS40YzIyLjEsMCw0MCwxNy45LDQwLDQwTDM0OS4yLDM1MS45ICAgTDM0OS4yLDM1MS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=",
component: SecurityPage,
},
];
function getPage() {
let pageid = window.location.hash.slice(1);
return pages.find((e) => e.id === pageid) || pages[0];
}
let page = getPage();
window.addEventListener("hashchange", () => {
page = getPage();
});
// $: title = pages.find(e => e.id === page).title;
const mq = window.matchMedia("(min-width: 45rem)");
let sidebar_button = !mq.matches;
mq.addEventListener("change", (ev) => {
sidebar_button = !ev.matches;
});
let sidebar_active = false;
function setPage(pageid) {
let pg = pages.find((e) => e.id === pageid);
if (!pg) {
throw new Error("Invalid Page " + pageid);
} else {
let url = new URL(window.location.href);
url.hash = pg.id;
window.history.pushState({}, pg.title, url);
page = getPage();
}
sidebar_active = false;
}
let loading = true;
import NavigationBar from "./NavigationBar.svelte";
<script lang="ts">
import Sidebar from "./Sidebar.svelte";
import { CurrentPage } from "./nav";
import PersonalInfo from "./pages/PersonalInfo.svelte";
import Security from "./pages/Security.svelte";
</script>
<div class:loading class="root">
<div class="app_container">
<div class="header">
{#if sidebar_button}
<button on:click={() => (sidebar_active = !sidebar_active)}>
<svg
id="Layer_1"
style="enable-background:new 0 0 32 32;"
version="1.1"
viewBox="0 0 32 32"
width="32px"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z
M28,14H4c-1.104,0-2,0.896-2,2
s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z
M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2
S29.104,22,28,22z"
/>
</svg>
</button>
{/if}
<h1>{page.title}</h1>
</div>
<div class="sidebar" class:sidebar-visible={sidebar_active}>
<NavigationBar open={setPage} {pages} active={page} />
</div>
<div class="content">
<svelte:component this={page.component} bind:loading />
</div>
<div class="footer" />
<div class="grid main-grid min-h-screen overflow-hidden">
<div>
<Sidebar />
</div>
<div class="overflow-auto p-4">
{#if $CurrentPage == "personal-info"}
<PersonalInfo />
{:else if $CurrentPage == "security"}
<Security />
{/if}
</div>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader" />
</div>
</div>
{/if}
<style>
.loading {
background-color: rgba(0, 0, 0, 0.04);
filter: blur(10px);
}
:root {
--sidebar-width: 250px;
}
.root {
height: 100%;
}
.app_container {
display: grid;
height: 100%;
grid-template-columns: auto 100%;
grid-template-rows: 60px auto 60px;
grid-template-areas:
"sidebar header"
"sidebar mc"
"sidebar footer";
}
.header {
grid-area: header;
background-color: var(--primary);
padding: 12px;
display: flex;
}
.header > h1 {
margin: 0;
padding: 0;
font-size: 24px;
line-height: 36px;
color: white;
margin-left: 2rem;
}
.header > button {
height: 36px;
background-color: transparent;
border: none;
font-size: 20px;
}
.header > button:hover {
background-color: rgba(255, 255, 255, 0.151);
}
.sidebar {
width: 0;
overflow: hidden;
grid-area: sidebar;
transition: width 0.2s;
background-color: lightgrey;
height: 100%;
}
.sidebar-visible {
width: var(--sidebar-width);
transition: width 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
grid-area: mc;
padding: 1rem;
}
.footer {
grid-area: footer;
}
@media (min-width: 45rem) {
.app_container {
grid-template-columns: auto 1fr;
}
.sidebar {
width: var(--sidebar-width);
transition: all 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
padding: 2rem;
}
}
.loader_container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
.main-grid {
grid-template-columns: auto 1fr;
}
</style>

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

View File

@ -0,0 +1,197 @@
<script lang="ts">
import {
type ContactInfo,
type Account,
Gender,
} from "@hibas123/openauth-internalapi";
import InternalAPI from "../../../helper/api";
import Loading from "../Loading.svelte";
import { onMount } from "svelte";
import {
Button,
Card,
Input,
Label,
Select,
Heading,
Spinner,
Helper,
} from "flowbite-svelte";
let profileInfo: Account;
let loadedProfileInfo: Account;
let contactInfo: ContactInfo;
let loading = true;
let error: string | undefined;
async function load() {
error = undefined;
loading = true;
try {
profileInfo = await InternalAPI.Account.GetProfile();
loadedProfileInfo = { ...profileInfo };
contactInfo = await InternalAPI.Account.GetContactInfos();
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
let savingProfile = false;
async function saveProfileChanges() {
savingProfile = true;
try {
await new Promise((yes) => setTimeout(yes, 1000));
await InternalAPI.Account.UpdateProfile(profileInfo);
loadedProfileInfo = { ...profileInfo };
} catch (e) {
error = e.message;
} finally {
savingProfile = false;
}
}
$: hasProfileChanged =
JSON.stringify(profileInfo) != JSON.stringify(loadedProfileInfo);
onMount(() => {
load();
});
let genders = [
{
value: Gender.None,
name: "Not saying",
},
{
value: Gender.Male,
name: "Male",
},
{
value: Gender.Female,
name: "Female",
},
{
value: Gender.Other,
name: "Other",
},
];
</script>
<Loading {loading} {error}>
<Card>
<Heading tag="h5">General Account Details</Heading>
<hr class="mb-6" />
<div class="mb-6">
<Label for="name-input" class="block mb-2">Name</Label>
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
</div>
<div class="mb-6">
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
<Input
id="birthday-input"
placeholder="Birthday"
disabled
bind:value={profileInfo.birthday}
/>
</div>
<div class="mb-6">
<Label class="block mb-2"
>Gender
<Select items={genders} bind:value={profileInfo.gender} />
</Label>
</div>
<Button
disabled={!hasProfileChanged || savingProfile}
on:click={saveProfileChanges}
>
{#if savingProfile}
<Spinner class="mr-3" size="4" color="white" /> Saving...
{:else}
Save
{/if}
</Button>
</Card>
<Card class="mt-4">
<Heading tag="h5">Contact Details (WIP)</Heading>
<hr class="mb-6" />
<Heading tag="h6" color="gray">Mails</Heading>
<hr class="mb-6" />
{#each contactInfo.mail as mail}
<div class="mb-6">
<!-- <Label for="mail-input" class="block mb-2">Mail</Label> -->
<Input
id="mail-input"
placeholder="Mail"
bind:value={mail.mail}
color={mail.verified ? "green" : "base"}
disabled
/>
{#if mail.verified}
<Helper class="mt-2" color="green"
><span class="font-medium">Well done!</span> E-Mail is verified.</Helper
>
{:else}
<Helper class="mt-2" color="gray"
><span class="font-medium">Oh no!</span> E-Mail needs verification.</Helper
>
{/if}
</div>
{/each}
<Heading tag="h6" color="gray">Phones</Heading>
<hr class="mb-6" />
{#each contactInfo.phone as phone}
<div class="mb-6">
<!-- <Label for="phone-input" class="block mb-2">Phone</Label> -->
<Input
id="phone-input"
placeholder="Phone"
bind:value={phone.phone}
color={phone.verified ? "green" : "base"}
disabled
/>
{#if phone.verified}
<Helper class="mt-2" color="green"
><span class="font-medium">Well done!</span> Phone is verified.</Helper
>
{:else}
<Helper class="mt-2" color="gray"
><span class="font-medium">Oh no!</span> Phone needs verification.</Helper
>
{/if}
</div>
{/each}
<!-- <div class="mb-6">
<Label for="name-input" class="block mb-2">Name</Label>
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
</div>
<div class="mb-6">
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
<Input
id="birthday-input"
placeholder="Birthday"
disabled
bind:value={profileInfo.birthday}
/>
</div>
<div class="mb-6">
<Label class="block mb-2"
>Gender
<Select items={genders} bind:value={profileInfo.gender} />
</Label>
</div> -->
<!-- <Button>Save</Button> -->
</Card>
</Loading>

View File

@ -1,188 +1,149 @@
<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 lang="ts">
import {
type ContactInfo,
type Account,
Gender,
Token,
TwoFactor,
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,
} from "flowbite-svelte";
<script>
import Box from "./Box.svelte";
import BoxItem from "./BoxItem.svelte";
import NextIcon from "./NextIcon.svelte";
import request from "../../../helper/request.ts";
let tokens: Token[];
let twofactors: TwoFactor[];
let error: string | undefined;
let loading = true;
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() {
async function load() {
loading = true;
let res = await request(
"/api/user/token",
undefined,
undefined,
undefined,
true,
true
);
token = res.token;
loading = false;
try {
tokens = await InternalAPI.Security.GetTokens();
twofactors = await InternalAPI.Security.GetTwofactorOptions();
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
loadToken();
loadTwoFactor();
onMount(() => {
load();
});
async function revokeToken(id: string) {
try {
await InternalAPI.Security.RevokeToken(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",
};
</script>
<style>
.btn {
background-color: var(--primary);
margin: auto 0;
margin-left: 1rem;
font-size: 1rem;
padding: 0 0.5rem;
}
<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>
.floating {
margin-bottom: 0;
}
<Card size="xl" class="mt-4">
<Heading tag="h5">Change Password</Heading>
<hr class="mb-6" />
.input-container {
display: flex;
}
<div class="mb-6">
<Label for="oldPassword">Old Password</Label>
<Input type="password" id="oldPassword" />
</div>
<div class="mb-6">
<Label for="newPassword">New Password</Label>
<Input type="password" id="newPassword" />
</div>
<div class="mb-6">
<Label for="newPasswordRepeat">Repeat New Password</Label>
<Input type="password" id="newPasswordRepeat" />
</div>
<Button class="mt-4">Change Password</Button>
</Card>
.input-container > *:first-child {
flex-grow: 1;
}
<Card size="xl" class="mt-4">
<Heading tag="h5">Two Factor Auth</Heading>
<hr class="mb-6" />
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%;
}
<Accordion>
{#each twofactors as tfa}
<AccordionItem>
<span slot="header">{tfa.name ?? typeToName[tfa.tfatype]}</span>
</AccordionItem>
{/each}
</Accordion>
<Button class="mt-4">Add Option</Button>
</Card>
select > option {
background-color: unset;
}
<!-- <Card size="xl" class="mt-4">
<Heading tag="h5">Delete Account</Heading>
<hr class="mb-6" />
.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>
<div class="mb-6">
<Label for="password">Password</Label>
<Input type="password" id="password" />
</div>
<Button class="mt-4">Delete Account</Button>
</Card> -->
</Loading>

View File

@ -0,0 +1,34 @@
<script>
import {
Sidebar,
SidebarGroup,
SidebarItem,
SidebarWrapper,
} from "flowbite-svelte";
import { CurrentPage } from "./nav";
</script>
<Sidebar class="h-screen">
<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>

View File

@ -1,4 +1,4 @@
import "../../components/theme";
import "../../main.css";
import App from "./App.svelte";
new App({

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

View File

@ -0,0 +1,207 @@
<script>
import AccountPage from "./Pages/Account.svelte";
import SecurityPage from "./Pages/Security.svelte";
import { slide, fade } from "svelte/transition";
const pages = [
{
id: "account",
title: "Account",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgdmlld0JveD0iMCAwIDUwMCA1MDAiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT4wMSBAZnVsbHdpZHRoPC90aXRsZT48ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz48ZyBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj48ZyBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDU3LjUsMjUwIEM0NTcuNSwxMzUuOTUzMTk5IDM2NS4wNDY4MDEsNDMuNSAyNTEsNDMuNSBDMTM2Ljk1MzE5OSw0My41IDQ0LjUsMTM1Ljk1MzE5OSA0NC41LDI1MCBDNDQuNSwzNjQuMDQ2ODAxIDEzNi45NTMxOTksNDU2LjUgMjUxLDQ1Ni41IEMzNjUuMDQ2ODAxLDQ1Ni41IDQ1Ny41LDM2NC4wNDY4MDEgNDU3LjUsMjUwIFogTTU3LjUsMjUwIEM1Ny41LDE0My4xMzI5MDEgMTQ0LjEzMjkwMSw1Ni41IDI1MSw1Ni41IEMzNTcuODY3MDk5LDU2LjUgNDQ0LjUsMTQzLjEzMjkwMSA0NDQuNSwyNTAgQzQ0NC41LDM1Ni44NjcwOTkgMzU3Ljg2NzA5OSw0NDMuNSAyNTEsNDQzLjUgQzE0NC4xMzI5MDEsNDQzLjUgNTcuNSwzNTYuODY3MDk5IDU3LjUsMjUwIFoiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD48cGF0aCBkPSJNMjUxLjUsMjUyLjkzMzk2MiBDMTk2Ljg1NDE5LDI1Mi45MzM5NjIgMTUyLjUsMjk2LjM2MDgwOSAxNTIuNSwzNTAgQzE1Mi41LDM1My41ODk4NTEgMTU1LjQxMDE0OSwzNTYuNSAxNTksMzU2LjUgTDM0NCwzNTYuNSBDMzQ3LjU4OTg1MSwzNTYuNSAzNTAuNSwzNTMuNTg5ODUxIDM1MC41LDM1MCBDMzUwLjUsMjk2LjM2MDgwOSAzMDYuMTQ1ODEsMjUyLjkzMzk2MiAyNTEuNSwyNTIuOTMzOTYyIFogTTE2NS41LDM0My41MDAwMDEgQzE2NS41LDMwMy42MDI3MDggMjAzLjk3MzEzMSwyNjUuOTMzOTYyIDI1MS41LDI2NS45MzM5NjIgQzI5OS4wMjY4NjksMjY1LjkzMzk2MiAzMzcuNSwzMDMuNjAyNzA4IDMzNy40OTk5OTcsMzQzLjUwMDAwMSBMMTY1LjUsMzQzLjUwMDAwMSBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PHBhdGggZD0iTTMwNC4yNSwxOTMuMzk2MjI2IEMzMDQuMjUsMTY1LjgwODIwMiAyODEuNDY1NDI0LDE0My41IDI1My40MjcwODMsMTQzLjUgQzIyNS4zODg3NDIsMTQzLjUgMjAyLjYwNDE2NywxNjUuODA4MjAyIDIwMi42MDQxNjcsMTkzLjM5NjIyNiBDMjAyLjYwNDE2NywyMjAuOTg0MjUgMjI1LjM4ODc0MiwyNDMuMjkyNDUzIDI1My40MjcwODMsMjQzLjI5MjQ1MyBDMjgxLjQ2NTQyNCwyNDMuMjkyNDUzIDMwNC4yNSwyMjAuOTg0MjUgMzA0LjI1LDE5My4zOTYyMjYgWiBNMjE1LjYwNDE2NywxOTMuMzk2MjI2IEMyMTUuNjA0MTY3LDE3My4wNTAxMDIgMjMyLjUwNzY4MywxNTYuNSAyNTMuNDI3MDgzLDE1Ni41IEMyNzQuMzQ2NDg0LDE1Ni41IDI5MS4yNSwxNzMuMDUwMTAyIDI5MS4yNSwxOTMuMzk2MjI2IEMyOTEuMjUsMjEzLjc0MjM1MSAyNzQuMzQ2NDg0LDIzMC4yOTI0NTMgMjUzLjQyNzA4MywyMzAuMjkyNDUzIEMyMzIuNTA3NjgzLDIzMC4yOTI0NTMgMjE1LjYwNDE2NywyMTMuNzQyMzUxIDIxNS42MDQxNjcsMTkzLjM5NjIyNiBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PC9nPjwvZz48L3N2Zz4=",
component: AccountPage,
},
{
id: "security",
title: "Security",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyIDUxMjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik00NDUuOCwzNy4zYy0xLjItMC43LTIuNy0wLjgtMy45LTAuMWMtNC4zLDIuMi0xMS42LDMuNC0yMS45LDMuNGMtMzMuNiwwLTkwLjgtMTIuNC0xMjguNy0yMC41Yy0xNC0zLTI2LjEtNS42LTM0LjEtNyAgIGMtMC40LTAuMS0wLjktMC4xLTEuNCwwYy03LjIsMS4yLTE4LjUsMy42LTMxLjcsNi4zQzE4NC4zLDI3LjYsMTI0LjIsNDAsOTAuNiw0MGMtMTEuNiwwLTE3LTEuNS0xOS41LTIuOCAgIGMtMS4yLTAuNi0yLjctMC42LTMuOSwwLjFzLTEuOSwyLTEuOSwzLjRjMCw3My4xLDMuOCwxNjguNCwzMy45LDI1Ny43YzE0LjMsNDIuOCwzMy41LDgwLjcsNTcuMSwxMTIuNyAgIGMyNi42LDM2LDU5LjcsNjUuNyw5OC4zLDg4LjNjMC42LDAuNCwxLjMsMC41LDIsMC41czEuNC0wLjIsMi0wLjVjMzguNi0yMi42LDcxLjYtNTIuMyw5OC4yLTg4LjNjMjMuNi0zMiw0Mi45LTY5LjksNTcuMS0xMTIuNyAgIGMyOS41LTg3LjYsMzMuNy0xNzkuNCwzMy45LTI1Ny43QzQ0Ny43LDM5LjQsNDQ3LDM4LjEsNDQ1LjgsMzcuM3ogTTQwNi4zLDI5NS45Yy0yOS4zLDg4LjEtNzkuNywxNTMuOC0xNDkuOCwxOTUuNCAgIEMxODYuNCw0NDkuNywxMzYsMzg0LDEwNi43LDI5NS45Qzc3LjgsMjEwLDczLjQsMTE4LjEsNzMuMiw0Ni40YzQuNSwxLjEsMTAuMiwxLjYsMTcuMywxLjZjMCwwLDAsMCwwLDAgICBjMzQuNSwwLDk1LjEtMTIuNiwxMzUuMi0yMC45YzEyLjctMi42LDIzLjYtNC45LDMwLjctNi4xYzcuOCwxLjQsMTkuNSwzLjksMzMuMSw2LjhDMzMwLDM2LjYsMzg1LjUsNDguNiw0MjAsNDguNiAgIGM4LjEsMCwxNC43LTAuNywxOS43LTJDNDM5LjMsMTIyLjksNDM0LjcsMjExLjUsNDA2LjMsMjk1Ljl6Ij48L3BhdGg+PHBhdGggZD0iTTI1Ni41LDIxNy44YzQ1LDAsODEuNi0zNi42LDgxLjYtODEuNmMwLTQ1LTM2LjYtODEuNi04MS42LTgxLjZjLTQ1LDAtODEuNiwzNi42LTgxLjYsODEuNiAgIEMxNzQuOSwxODEuMiwyMTEuNSwyMTcuOCwyNTYuNSwyMTcuOHogTTI1Ni41LDYyLjZjNDAuNiwwLDczLjYsMzMsNzMuNiw3My42YzAsNDAuNi0zMyw3My42LTczLjYsNzMuNmMtNDAuNiwwLTczLjYtMzMtNzMuNi03My42ICAgQzE4Mi45LDk1LjYsMjE1LjksNjIuNiwyNTYuNSw2Mi42eiI+PC9wYXRoPjxwYXRoIGQ9Ik0zMDkuMiwyMjguOUgyMDMuOGMtMjYuNSwwLTQ4LDIxLjUtNDgsNDh2NzljMCwyLjIsMS44LDQsNCw0aDE5My40YzIuMiwwLDQtMS44LDQtNHYtNzkgICBDMzU3LjIsMjUwLjQsMzM1LjYsMjI4LjksMzA5LjIsMjI4Ljl6IE0zNDkuMiwzNTEuOUgxNjMuOHYtNzVjMC0yMiwxNy45LTQwLDQwLTQwaDEwNS40YzIyLjEsMCw0MCwxNy45LDQwLDQwTDM0OS4yLDM1MS45ICAgTDM0OS4yLDM1MS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=",
component: SecurityPage,
},
];
function getPage() {
let pageid = window.location.hash.slice(1);
return pages.find((e) => e.id === pageid) || pages[0];
}
let page = getPage();
window.addEventListener("hashchange", () => {
page = getPage();
});
// $: title = pages.find(e => e.id === page).title;
const mq = window.matchMedia("(min-width: 45rem)");
let sidebar_button = !mq.matches;
mq.addEventListener("change", (ev) => {
sidebar_button = !ev.matches;
});
let sidebar_active = false;
function setPage(pageid) {
let pg = pages.find((e) => e.id === pageid);
if (!pg) {
throw new Error("Invalid Page " + pageid);
} else {
let url = new URL(window.location.href);
url.hash = pg.id;
window.history.pushState({}, pg.title, url);
page = getPage();
}
sidebar_active = false;
}
let loading = true;
import NavigationBar from "./NavigationBar.svelte";
</script>
<div class:loading class="root">
<div class="app_container">
<div class="header">
{#if sidebar_button}
<button on:click={() => (sidebar_active = !sidebar_active)}>
<svg
id="Layer_1"
style="enable-background:new 0 0 32 32;"
version="1.1"
viewBox="0 0 32 32"
width="32px"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z
M28,14H4c-1.104,0-2,0.896-2,2
s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z
M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2
S29.104,22,28,22z"
/>
</svg>
</button>
{/if}
<h1>{page.title}</h1>
</div>
<div class="sidebar" class:sidebar-visible={sidebar_active}>
<NavigationBar open={setPage} {pages} active={page} />
</div>
<div class="content">
<svelte:component this={page.component} bind:loading />
</div>
<div class="footer" />
</div>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader" />
</div>
</div>
{/if}
<style>
.loading {
background-color: rgba(0, 0, 0, 0.04);
filter: blur(10px);
}
:root {
--sidebar-width: 250px;
}
.root {
height: 100%;
}
.app_container {
display: grid;
height: 100%;
grid-template-columns: auto 100%;
grid-template-rows: 60px auto 60px;
grid-template-areas:
"sidebar header"
"sidebar mc"
"sidebar footer";
}
.header {
grid-area: header;
background-color: var(--primary);
padding: 12px;
display: flex;
}
.header > h1 {
margin: 0;
padding: 0;
font-size: 24px;
line-height: 36px;
color: white;
margin-left: 2rem;
}
.header > button {
height: 36px;
background-color: transparent;
border: none;
font-size: 20px;
}
.header > button:hover {
background-color: rgba(255, 255, 255, 0.151);
}
.sidebar {
width: 0;
overflow: hidden;
grid-area: sidebar;
transition: width 0.2s;
background-color: lightgrey;
height: 100%;
}
.sidebar-visible {
width: var(--sidebar-width);
transition: width 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
grid-area: mc;
padding: 1rem;
}
.footer {
grid-area: footer;
}
@media (min-width: 45rem) {
.app_container {
grid-template-columns: auto 1fr;
}
.sidebar {
width: var(--sidebar-width);
transition: all 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
padding: 2rem;
}
}
.loader_container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
}
</style>

View File

@ -0,0 +1,188 @@
<script context="module">
const TFATypes = new Map();
TFATypes.set(0, "Authenticator");
TFATypes.set(1, "Backup Codes");
TFATypes.set(2, "YubiKey");
TFATypes.set(3, "Push Notification");
</script>
<script>
import Box from "./Box.svelte";
import BoxItem from "./BoxItem.svelte";
import NextIcon from "./NextIcon.svelte";
import request from "../../../helper/request.ts";
export let loading = false;
let twofactor = [];
async function deleteTFA(id) {
let res = await request(
"/api/user/twofactor/" + id,
undefined,
"DELETE",
undefined,
true,
true
);
loadTwoFactor();
}
async function loadTwoFactor() {
let res = await request(
"/api/user/twofactor",
undefined,
undefined,
undefined,
true,
true
);
twofactor = res.methods;
}
let token = [];
async function revoke(id) {
let res = await request(
"/api/user/token/" + id,
undefined,
"DELETE",
undefined,
true,
true
);
loadToken();
}
async function loadToken() {
loading = true;
let res = await request(
"/api/user/token",
undefined,
undefined,
undefined,
true,
true
);
token = res.token;
loading = false;
}
loadToken();
loadTwoFactor();
</script>
<style>
.btn {
background-color: var(--primary);
margin: auto 0;
margin-left: 1rem;
font-size: 1rem;
padding: 0 0.5rem;
}
.floating {
margin-bottom: 0;
}
.input-container {
display: flex;
}
.input-container > *:first-child {
flex-grow: 1;
}
select {
background-color: unset;
border: 0;
border-radius: 0;
color: unset;
font-size: unset;
border-bottom: 1px solid #757575;
/* Firefox */
-moz-appearance: none;
/* Safari and Chrome */
-webkit-appearance: none;
appearance: none;
height: 100%;
width: 100%;
}
select > option {
background-color: unset;
}
.select-wrapper {
position: relative;
}
.select-wrapper::after {
content: ">";
display: block;
position: absolute;
right: 2rem;
top: 0;
bottom: 0;
width: 1rem;
transform: rotate(90deg) scaleY(2);
}
</style>
<Box>
<h1>Two Factor</h1>
<BoxItem name="Add new" open={false} />
{#each twofactor as t}
<BoxItem name={TFATypes.get(t.type)} value={t.name} highlight={t.isthis}>
<button
class="btn"
style="background: var(--error)"
on:click={() => deleteTFA(t.id)}>
Delete
</button>
</BoxItem>
{/each}
<!-- <BoxItem name="Name" value={name} open={false}>
<div class="input-container">
<div class="floating group">
<input type="text" autocomplete="username" bind:value={name}>
<span class="highlight"></span>
<span class="bar"></span>
<label>Name</label>
</div>
<button class="btn" on:click={saveName}>Save</button>
</div>
</BoxItem>
<BoxItem name="Gender" value={gender} open={true}>
<div class="input-container">
<div class="select-wrapper">
<select>
<option value="1">Male</option>
<option value="2">Female</option>
<option value="3">Other</option>
</select>
</div>
<button class="btn" on:click={saveName}>Save</button>
</div>
</BoxItem>
<BoxItem name="Birthday" value={birthday} />
<BoxItem name="Password" value="******" /> -->
</Box>
<Box>
<h1>Anmeldungen</h1>
{#each token as t}
<BoxItem name={t.browser} value={t.ip} highlight={t.isthis}>
<button
class="btn"
style="background: var(--error)"
on:click={() => revoke(t.id)}>
Revoke
</button>
</BoxItem>
{:else}<span>No Tokens</span>{/each}
<!-- <BoxItem name="E-Mail" value={email} />
<BoxItem name="Phone" value={phone} /> -->
</Box>

View File

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

Binary file not shown.

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

77
InternalAPI/api.jrpc Normal file
View File

@ -0,0 +1,77 @@
type UserRegisterInfo {
username: string;
name: string;
gender: string;
mail: string;
password: string;
salt: string;
}
type Token {
id: string;
special: boolean;
ip: string;
browser: string;
isthis: boolean;
}
enum Gender {
None = 0,
Male = 1,
Female = 2,
Other = 3
}
type Account {
id: string;
name: string;
username: string;
birthday: int;
gender: Gender;
}
type Mail {
mail: string;
verified: boolean;
primary: boolean;
}
type Phone {
phone: string;
verified: boolean;
primary: boolean;
}
type ContactInfo {
mail: Mail[];
phone: Phone[];
}
enum TFAType {
TOTP = 0,
BACKUP_CODE = 1,
WEBAUTHN = 2,
APP_ALLOW = 3
}
type TwoFactor {
id: string;
name?: string;
expires?: int;
tfatype: TFAType;
}
service AccountService {
Register(regcode: string, info: UserRegisterInfo): void;
GetProfile(): Account;
UpdateProfile(info: Account): void;
GetContactInfos(): ContactInfo;
}
service SecurityService {
GetTokens(): Token[];
RevokeToken(id: string): void;
GetTwofactorOptions(): TwoFactor[];
}

3
_API/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
lib/
esm/
src/

19
_API/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "@hibas123/openauth-internalapi",
"private": true,
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json && tsc -p tsconfig-esm.json",
"dev": "tsc -w"
},
"keywords": [],
"author": "Fabian Stamm <Fabian.Stamm@polizei.hessen.de>",
"license": "ISC",
"devDependencies": {
"typescript": "^5.0.2"
}
}

21
_API/tsconfig-esm.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "esnext",
"moduleResolution": "node",
"outDir": "esm/",
"sourceMap": true,
"declaration": true,
"noImplicitAny": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"preserveWatchOutput": true
},
"exclude": [
"node_modules"
],
"include": [
"src"
]
}

21
_API/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"moduleResolution": "node",
"outDir": "lib/",
"sourceMap": true,
"declaration": true,
"noImplicitAny": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"preserveWatchOutput": true
},
"exclude": [
"node_modules"
],
"include": [
"src"
]
}

View File

@ -5,6 +5,7 @@
"private": true,
"scripts": {
"build": "yarn run build-views-1 && yarn run build-views-2 && yarn run build-backend",
"build-api": "jrpc compile ./InternalAPI/api.jrpc -o=ts-node:_API/src && yarn workspace @hibas123/openauth-internalapi run build",
"build-backend": "yarn workspace @hibas123/openauth-backend run build",
"build-views-1": "yarn workspace @hibas123/openauth-views-v1 run build",
"build-views-2": "yarn workspace @hibas123/openauth-views-v2 run build",
@ -13,6 +14,10 @@
"workspaces": [
"Backend",
"Frontend",
"FrontendLegacy"
]
"FrontendLegacy",
"_API"
],
"dependencies": {
"@hibas123/jrpcgen": "^1.2.11"
}
}

978
yarn.lock

File diff suppressed because it is too large Load Diff