Start implementing a new user page for account and security settings
This commit is contained in:
parent
1e2bb83447
commit
922ed1e813
@ -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",
|
||||
|
@ -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);
|
||||
|
||||
|
49
Backend/src/api/jrpc/account_service.ts
Normal file
49
Backend/src/api/jrpc/account_service.ts
Normal 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;
|
||||
}
|
||||
}
|
45
Backend/src/api/jrpc/index.ts
Normal file
45
Backend/src/api/jrpc/index.ts
Normal 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;
|
71
Backend/src/api/jrpc/security_service.ts
Normal file
71
Backend/src/api/jrpc/security_service.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
})
|
||||
|
@ -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!",
|
||||
|
@ -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!",
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
cssnano: {},
|
||||
},
|
||||
};
|
||||
|
@ -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,22 +67,11 @@ 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",
|
||||
{
|
||||
file: `build/${view}/bundle.min.js`,
|
||||
format: "es",
|
||||
sourcemap: true,
|
||||
name: view,
|
||||
}
|
||||
: {
|
||||
file: `build/${view}/bundle.min.js`,
|
||||
format: "iife",
|
||||
name: view,
|
||||
plugins: [
|
||||
esbuild({
|
||||
minify: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
@ -89,7 +79,8 @@ export default VIEWS.map((view) => ({
|
||||
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(),
|
||||
|
23
Frontend/src/helper/api.ts
Normal file
23
Frontend/src/helper/api.ts
Normal 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;
|
@ -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
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">
|
||||
<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>
|
||||
|
@ -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>
|
||||
<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}
|
||||
<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 {
|
||||
.main-grid {
|
||||
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>
|
||||
|
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}
|
197
Frontend/src/pages/user/Pages/PersonalInfo.svelte
Normal file
197
Frontend/src/pages/user/Pages/PersonalInfo.svelte
Normal 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>
|
@ -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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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)}>
|
||||
<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
|
||||
</button>
|
||||
</BoxItem>
|
||||
{:else}<span>No Tokens</span>{/each}
|
||||
</a>
|
||||
</TableBodyCell>
|
||||
</TableBodyRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<!-- <BoxItem name="E-Mail" value={email} />
|
||||
<BoxItem name="Phone" value={phone} /> -->
|
||||
</Box>
|
||||
<Card size="xl" class="mt-4">
|
||||
<Heading tag="h5">Change Password</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</AccordionItem>
|
||||
{/each}
|
||||
</Accordion>
|
||||
<Button class="mt-4">Add Option</Button>
|
||||
</Card>
|
||||
|
||||
<!-- <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>
|
||||
|
34
Frontend/src/pages/user/Sidebar.svelte
Normal file
34
Frontend/src/pages/user/Sidebar.svelte
Normal 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>
|
@ -1,4 +1,4 @@
|
||||
import "../../components/theme";
|
||||
import "../../main.css";
|
||||
import App from "./App.svelte";
|
||||
|
||||
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;
|
||||
}
|
||||
|
207
Frontend/src/pages/user_old/App.svelte
Normal file
207
Frontend/src/pages/user_old/App.svelte
Normal 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>
|
188
Frontend/src/pages/user_old/Pages/Security.svelte
Normal file
188
Frontend/src/pages/user_old/Pages/Security.svelte
Normal 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>
|
6
Frontend/src/pages/user_old/main.ts
Normal file
6
Frontend/src/pages/user_old/main.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import "../../components/theme";
|
||||
import App from "./App.svelte";
|
||||
|
||||
new App({
|
||||
target: document.body,
|
||||
});
|
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",
|
||||
};
|
77
InternalAPI/api.jrpc
Normal file
77
InternalAPI/api.jrpc
Normal 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
3
_API/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
lib/
|
||||
esm/
|
||||
src/
|
19
_API/package.json
Normal file
19
_API/package.json
Normal 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
21
_API/tsconfig-esm.json
Normal 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
21
_API/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user