Start implementing a new user page for account and security settings
This commit is contained in:
		| @ -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 { | ||||
| @ -53,7 +53,7 @@ const TwoFactor = DB.addModel<ITwoFactor>({ | ||||
|    name: "twofactor", | ||||
|    versions: [ | ||||
|       { | ||||
|          migration: (e) => {}, | ||||
|          migration: (e) => { }, | ||||
|          schema: { | ||||
|             user: { type: ObjectID }, | ||||
|             valid: { type: Boolean }, | ||||
|  | ||||
| @ -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" | ||||
|    } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Fabian Stamm
					Fabian Stamm